Port PSN pkg decryption to pure Python (drop Wine requirement)
This commit is contained in:
+45
-12
@@ -2,9 +2,9 @@
|
||||
|
||||
NoPayStation publishes per-platform TSV lists (``https://nopaystation.com/tsv/``)
|
||||
with one row per downloadable PSN item. Each row links to an encrypted PSN
|
||||
``.pkg`` (a ``\\x7FPKG`` container, not a ZIP) that must be decrypted with the
|
||||
``pkg2zip`` tool before it can be installed as a ``PSP/GAME/<TITLE_ID>/``
|
||||
folder.
|
||||
``.pkg`` (a ``\\x7FPKG`` container, not a ZIP) that is decrypted by the bundled
|
||||
pure-Python decryptor (``app/pkg.py``) into a ``PSP/GAME/<TITLE_ID>/``-style
|
||||
folder — no Wine or ``pkg2zip`` required.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -173,22 +173,55 @@ def _safe_folder(value: str) -> str:
|
||||
return cleaned or "PKG"
|
||||
|
||||
|
||||
def extract_pkg(pkg_path, title_id: str, dest_dir, pkg2zip: Path,
|
||||
progress_cb=None, cancel_event=None) -> Path:
|
||||
"""Decrypt/extract a ``.pkg`` with pkg2zip into ``dest_dir/<TITLE_ID>/``.
|
||||
def _ensure_keys_bin(folder, kind):
|
||||
"""PS1 Classics need KEYS.BIN next to EBOOT.PBP to boot on CFW (npdrm)."""
|
||||
if kind != "psx":
|
||||
return
|
||||
src = config.DATA_DIR / "loader" / "KEYS.BIN"
|
||||
dest = Path(folder) / "KEYS.BIN"
|
||||
if src.is_file() and not dest.exists():
|
||||
try:
|
||||
shutil.copy2(src, dest)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
pkg2zip extracts PSP/PSX content into a folder layout we then normalize:
|
||||
the game's ``EBOOT.PBP`` (plus siblings) are copied into the destination
|
||||
game folder. Returns the destination folder path.
|
||||
|
||||
def extract_pkg(pkg_path, title_id: str, dest_dir, pkg2zip: Path = None,
|
||||
progress_cb=None, cancel_event=None) -> Path:
|
||||
"""Decrypt/extract a ``.pkg`` into ``dest_dir/<TITLE_ID>/``.
|
||||
|
||||
Uses the bundled pure-Python decryptor (no Wine / pkg2zip needed); falls
|
||||
back to pkg2zip only if that fails and a binary is available. Returns the
|
||||
destination game folder path.
|
||||
"""
|
||||
from . import pkg as pkgmod
|
||||
from .store import TransferCancelled
|
||||
|
||||
pkg_path = Path(pkg_path)
|
||||
folder_name = _safe_folder(title_id)
|
||||
dest = Path(dest_dir) / folder_name
|
||||
dest = Path(dest_dir) / _safe_folder(title_id)
|
||||
if (dest / "EBOOT.PBP").exists():
|
||||
return dest
|
||||
|
||||
# 1. Pure-Python decryptor (primary).
|
||||
try:
|
||||
_kind, _tid, folder = pkgmod.extract(pkg_path, dest_dir, progress_cb, cancel_event)
|
||||
_ensure_keys_bin(folder, _kind)
|
||||
return folder
|
||||
except TransferCancelled:
|
||||
raise
|
||||
except Exception as exc: # noqa: BLE001
|
||||
py_exc = exc
|
||||
log.warning("python pkg decrypt failed (%s); trying pkg2zip fallback", exc)
|
||||
|
||||
# 2. pkg2zip fallback (needs a binary; usually Wine + the bundled .exe).
|
||||
if pkg2zip is None:
|
||||
pkg2zip = find_pkg2zip()
|
||||
if pkg2zip is None:
|
||||
raise RuntimeError(
|
||||
f"could not decrypt {pkg_path.name} (no pkg2zip available): {py_exc}"
|
||||
)
|
||||
|
||||
folder_name = _safe_folder(title_id)
|
||||
tmp = Path(dest_dir).with_name(".nps-tmp-" + folder_name)
|
||||
if tmp.exists():
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
@@ -197,7 +230,7 @@ def extract_pkg(pkg_path, title_id: str, dest_dir, pkg2zip: Path,
|
||||
if progress_cb is not None:
|
||||
progress_cb("Extracting", 0.0, "")
|
||||
try:
|
||||
cmd = _pkg2zip_cmd(pkg2zip) + ["-x", str(pkg_path)]
|
||||
cmd = _pkg2zip_cmd(Path(pkg2zip)) + ["-x", str(pkg_path)]
|
||||
env = dict(os.environ)
|
||||
env["WINEDEBUG"] = "-all" # silence Wine's console chatter
|
||||
proc = subprocess.Popen(
|
||||
|
||||
Reference in New Issue
Block a user