From 5089caf2a02b9ecf9da5ea5ffb651a04fe89068e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Fri, 21 Aug 2026 16:53:07 +0200 Subject: [PATCH] Add a reader for PKG game update packages PSP game updates were distributed as NPDRM .pkg files holding a patched EBOOT (PBOOT.PBP) plus the data files the patch replaces. PkgUnpack reads one: header, item table, both PARAM.SFOs, and the AES-128-CTR that covers everything past the header - including the per-item key split, where an item's pspType byte picks between the PSP and PS3 keys. Nothing new is needed to decrypt these. All packages checked use PRX tag 0x2E5E10F0 for their PBOOT, which PrxDecrypter already has a key for. Also adds "PPSSPPHeadless --install-pkg=DIR", a sibling of --unpack-updater, which installs without any UI. All packages install through it byte -identically to a reference implementation. Format notes are in docs/pkg_notes.md. Co-Authored-By: Claude Opus 5 (1M context) --- Core/CMakeLists.txt | 2 + Core/CmdLine.cpp | 1 + Core/CmdLine.h | 6 + Core/Core.vcxproj | 2 + Core/Core.vcxproj.filters | 6 + Core/Util/PkgUnpack.cpp | 498 ++++++++++++++++++++++++++++ Core/Util/PkgUnpack.h | 108 ++++++ UWP/CoreUWP/CoreUWP.vcxproj | 2 + UWP/CoreUWP/CoreUWP.vcxproj.filters | 2 + android/jni/Android.mk | 1 + headless/Headless.cpp | 33 ++ libretro/Makefile.common | 1 + 12 files changed, 662 insertions(+) create mode 100644 Core/Util/PkgUnpack.cpp create mode 100644 Core/Util/PkgUnpack.h diff --git a/Core/CMakeLists.txt b/Core/CMakeLists.txt index 6a5bfad016..1cdc681e25 100644 --- a/Core/CMakeLists.txt +++ b/Core/CMakeLists.txt @@ -699,6 +699,8 @@ add_library(Core STATIC Util/KL4E.h Util/PSARUnpack.cpp Util/PSARUnpack.h + Util/PkgUnpack.cpp + Util/PkgUnpack.h Util/RecentFiles.cpp Util/RecentFiles.h ${CMAKE_SOURCE_DIR}/ext/disarm.cpp diff --git a/Core/CmdLine.cpp b/Core/CmdLine.cpp index 81f0f3b505..25ce9f2a14 100644 --- a/Core/CmdLine.cpp +++ b/Core/CmdLine.cpp @@ -211,6 +211,7 @@ static const CommandLineParam g_autoParams[] = { {POFF(unpackUpdater), CmdParamType::String, "unpack-updater", '\0', "Unpack the firmware in an updater EBOOT.PBP into DIR and exit", CmdLineMode::Headless}, {POFF(unpackUpdaterModel), CmdParamType::String, "unpack-updater-model", '\0', "PSP model to unpack for (01g..12g, default any)", CmdLineMode::Headless}, {POFF(unpackUpdaterFilter), CmdParamType::String, "unpack-updater-filter", '\0', "Only unpack entries under this path, e.g. flash0:/font/", CmdLineMode::Headless}, + {POFF(installPkg), CmdParamType::String, "install-pkg", '\0', "Install the game update in a .pkg into DIR and exit", CmdLineMode::Headless}, {POFF(odsLog), CmdParamType::Bool, "odslog", 'o', "Also log through OutputDebugString (Windows)", CmdLineMode::Headless}, {POFF(generateInterpreterDispatch), CmdParamType::Bool, "generate-interpreter-dispatch", '\0', "Generate C++ interpreter dispatch code (ExecInstruction) to stdout and exit", CmdLineMode::Headless}, {POFF(resolutionScale), CmdParamType::Int, "resolution-scale", '\0', "Set the resolution scale factor"}, diff --git a/Core/CmdLine.h b/Core/CmdLine.h index fdcb755277..0abf20a8e5 100644 --- a/Core/CmdLine.h +++ b/Core/CmdLine.h @@ -82,6 +82,12 @@ struct CommandLineOptions { // libraries. Needs a firmware dump under the NAND directory. std::optional disableHLE; + // Headless: install the game update in a .pkg (given as the boot filename) into this + // directory, then exit without booting anything. The directory is the game folder itself - + // the app puts that under PSP/GAME/, but here the caller picks. See + // Core/Util/PkgUnpack.h. + std::optional installPkg; + std::optional memReadAction; std::optional memWriteAction; std::optional breakAction; diff --git a/Core/Core.vcxproj b/Core/Core.vcxproj index b1b51f8aad..c5b395d226 100644 --- a/Core/Core.vcxproj +++ b/Core/Core.vcxproj @@ -924,6 +924,7 @@ + MaxSpeed true @@ -1294,6 +1295,7 @@ + diff --git a/Core/Core.vcxproj.filters b/Core/Core.vcxproj.filters index a7de4d3b8a..7d94c8a878 100644 --- a/Core/Core.vcxproj.filters +++ b/Core/Core.vcxproj.filters @@ -369,6 +369,9 @@ Util + + Util + HLE\Libraries @@ -1698,6 +1701,9 @@ Util + + Util + HLE\Libraries diff --git a/Core/Util/PkgUnpack.cpp b/Core/Util/PkgUnpack.cpp new file mode 100644 index 0000000000..6c1d4b5738 --- /dev/null +++ b/Core/Util/PkgUnpack.cpp @@ -0,0 +1,498 @@ +// Copyright (c) 2026- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +#include +#include + +#include "Common/File/FileUtil.h" +#include "Common/File/Path.h" +#include "Common/Log.h" +#include "Common/StringUtils.h" +#include "Core/ELF/ParamSFO.h" +#include "Core/Loaders.h" +#include "Core/Util/PkgUnpack.h" + +extern "C" { +#include "ext/libkirk/AES.h" +} + +// See docs/pkg_notes.md. Field offsets in the 0xC0-byte header: +static const u32 PKG_MAGIC = 0x7F504B47; // "\x7FPKG" +static const u32 PKG_TYPE_PSP = 2; // 1 is PS3. +// 0xC0 of header plus the 0x40-byte extended header after it, which is where the key index is. +static const size_t PKG_HEADER_SIZE = 0x100; +static const size_t PKG_ITEM_SIZE = 0x20; + +// The two keys a PSP package's contents are encrypted with, picked per item. Vita packages derive +// theirs from the riv instead, which we don't handle - nothing here reads Vita packages. +static const u8 PKG_PSP_KEY[16] = { + 0x07, 0xf2, 0xc6, 0x82, 0x90, 0xb5, 0x0d, 0x2c, 0x33, 0x81, 0x8d, 0x70, 0x9b, 0x60, 0xe6, 0x2b, +}; +static const u8 PKG_PS3_KEY[16] = { + 0x2e, 0x7b, 0x71, 0xd7, 0xc9, 0xc9, 0xa1, 0x4e, 0xa3, 0x22, 0x1f, 0x18, 0x88, 0x28, 0xb8, 0xf8, +}; + +// An item table with more entries than this is corrupt, not something we should try to allocate +// for. The biggest update package seen has a few hundred. +static const u32 PKG_MAX_ITEMS = 65536; +// Same idea for a single filename. +static const u32 PKG_MAX_NAME = 1024; + +static const size_t PKG_COPY_BLOCK = 512 * 1024; + +// Everything in a PKG header, its metadata and its item table is big-endian; the PBP inside is +// little-endian like the rest of the PSP world. None of it is guaranteed aligned, so read bytes. +static u16 Read16BE(const u8 *p) { + return (u16)(((u32)p[0] << 8) | p[1]); +} +static u32 Read32BE(const u8 *p) { + return ((u32)p[0] << 24) | ((u32)p[1] << 16) | ((u32)p[2] << 8) | (u32)p[3]; +} +static u64 Read64BE(const u8 *p) { + return ((u64)Read32BE(p) << 32) | (u64)Read32BE(p + 4); +} +static u32 Read32LE(const u8 *p) { + return ((u32)p[3] << 24) | ((u32)p[2] << 16) | ((u32)p[1] << 8) | (u32)p[0]; +} + +// AES-128-CTR, with the counter block starting at `iv` and incrementing once per 16 bytes. Both +// the counter and the increment are big-endian, and the counter wraps over the whole 128 bits. +static void AesCtrXor(const u8 *key, const u8 *iv, u64 blockIndex, u8 *data, size_t size) { + AES_ctx ctx; + AES_set_key(&ctx, key, 128); + + u8 counter[16]; + memcpy(counter, iv, 16); + // Add blockIndex to the 128-bit big-endian counter. + u32 carry = 0; + for (int i = 15; i >= 0; i--) { + const u32 sum = (u32)counter[i] + (u32)(blockIndex & 0xFF) + carry; + counter[i] = (u8)sum; + carry = sum >> 8; + blockIndex >>= 8; + } + + u8 stream[16]; + for (size_t pos = 0; pos < size; pos += 16) { + AES_encrypt(&ctx, counter, stream); + const size_t chunk = std::min(16, size - pos); + for (size_t i = 0; i < chunk; i++) { + data[pos + i] ^= stream[i]; + } + // Increment the counter block. + for (int i = 15; i >= 0; i--) { + if (++counter[i] != 0) + break; + } + } +} + +const u8 *PkgReader::ItemKey(const PkgItem &item) const { + return item.pspType == 0x90 ? mainKey_ : PKG_PS3_KEY; +} + +bool PkgReader::ReadEncrypted(u64 offset, size_t size, const u8 *key, u8 *out) { + if (size == 0) { + return true; + } + // The counter runs on 16-byte boundaries, so read from the containing block and skip the + // leading bytes afterwards. + const size_t skip = (size_t)(offset & 0xF); + const u64 base = offset - skip; + const size_t alignedSize = (skip + size + 15) & ~(size_t)0xF; + + if (base > dataSize_ || alignedSize > dataSize_ - base) { + return false; + } + + std::vector buf(alignedSize); + if (loader_->ReadAt(dataOffset_ + base, alignedSize, buf.data()) != alignedSize) { + return false; + } + AesCtrXor(key, riv_, base / 16, buf.data(), alignedSize); + memcpy(out, buf.data() + skip, size); + return true; +} + +bool PkgReader::ReadItemData(const PkgItem &item, u64 offset, size_t size, u8 *out) { + if (offset > item.dataSize || size > item.dataSize - offset) { + return false; + } + return ReadEncrypted(item.dataOffset + offset, size, ItemKey(item), out); +} + +bool PkgReader::ReadItem(const PkgItem &item, std::vector *out, size_t maxSize) { + if (item.dataSize > maxSize) { + return false; + } + out->resize((size_t)item.dataSize); + return ReadItemData(item, 0, out->size(), out->data()); +} + +bool PkgReader::Open(FileLoader *loader, std::string *error) { + loader_ = loader; + info_ = PkgInfo(); + + u8 header[PKG_HEADER_SIZE]; + if (!loader || loader->ReadAt(0, sizeof(header), header) != sizeof(header)) { + *error = "Not a PKG file: too short"; + return false; + } + if (Read32BE(header) != PKG_MAGIC) { + *error = "Not a PKG file"; + return false; + } + + const u32 type = Read16BE(header + 0x06); + if (type != PKG_TYPE_PSP) { + *error = "Not a PSP PKG file"; + return false; + } + // Key index 1 is the only one a PSP package uses; 2-4 are Vita, and derive a key from the riv. + const u32 keyIndex = header[0xE7] & 7; + if (keyIndex != 1) { + *error = StringFromFormat("Unsupported PKG key index %d", keyIndex); + return false; + } + memcpy(mainKey_, PKG_PSP_KEY, sizeof(mainKey_)); + + const u32 metaOffset = Read32BE(header + 0x08); + const u32 metaCount = Read32BE(header + 0x0C); + const u32 itemCount = Read32BE(header + 0x14); + const u64 totalSize = Read64BE(header + 0x18); + dataOffset_ = Read64BE(header + 0x20); + dataSize_ = Read64BE(header + 0x28); + memcpy(riv_, header + 0x70, sizeof(riv_)); + + char contentId[0x31]{}; + memcpy(contentId, header + 0x30, 0x30); + info_.contentId = contentId; + + const s64 fileSize = loader->FileSize(); + if (fileSize < 0 || (u64)fileSize < totalSize || dataOffset_ + dataSize_ > (u64)fileSize) { + *error = "PKG file is truncated"; + return false; + } + if (itemCount == 0 || itemCount > PKG_MAX_ITEMS) { + *error = "PKG file has a broken item table"; + return false; + } + + // The metadata is in the clear. We only need three things out of it, and one of them (the item + // table offset) is zero in every update package seen - but read it rather than assume. + u32 itemsOffset = 0; + u64 metaPos = metaOffset; + for (u32 i = 0; i < metaCount; i++) { + u8 rec[8]; + if (loader->ReadAt(metaPos, sizeof(rec), rec) != sizeof(rec)) { + *error = "PKG metadata is truncated"; + return false; + } + const u32 id = Read32BE(rec); + const u32 size = Read32BE(rec + 4); + if (size > 0x1000) { + *error = "PKG metadata is corrupt"; + return false; + } + std::vector value(size); + if (size && loader->ReadAt(metaPos + 8, size, value.data()) != size) { + *error = "PKG metadata is truncated"; + return false; + } + switch (id) { + case 2: + if (size >= 4) { + info_.contentType = Read32BE(value.data()); + } + break; + case 6: + info_.titleId = std::string((const char *)value.data(), strnlen((const char *)value.data(), size)); + break; + case 13: + if (size >= 4) { + itemsOffset = Read32BE(value.data()); + } + break; + default: + break; + } + metaPos += 8 + size; + } + + if (info_.contentType != kPkgContentTypePSP) { + *error = StringFromFormat("PKG holds content type 0x%x, not a PSP game", info_.contentType); + return false; + } + + // Item table, then the filenames it points at. Both live in the encrypted area, but a + // filename is encrypted with its own item's key rather than the table's. + std::vector table((size_t)itemCount * PKG_ITEM_SIZE); + if (!ReadEncrypted(itemsOffset, table.size(), mainKey_, table.data())) { + *error = "Failed to read the PKG item table"; + return false; + } + + info_.items.reserve(itemCount); + for (u32 i = 0; i < itemCount; i++) { + const u8 *rec = table.data() + (size_t)i * PKG_ITEM_SIZE; + const u32 nameOffset = Read32BE(rec); + const u32 nameSize = Read32BE(rec + 4); + + PkgItem item; + item.dataOffset = Read64BE(rec + 8); + item.dataSize = Read64BE(rec + 16); + item.pspType = rec[0x18]; + item.flags = rec[0x1B]; + + if (item.dataOffset > dataSize_ || item.dataSize > dataSize_ - item.dataOffset) { + *error = "PKG item points outside the file"; + return false; + } + if (nameSize == 0 || nameSize > PKG_MAX_NAME) { + *error = "PKG item has a broken name"; + return false; + } + item.name.resize(nameSize); + if (!ReadEncrypted(nameOffset, nameSize, ItemKey(item), (u8 *)item.name.data())) { + *error = "Failed to read a PKG item name"; + return false; + } + // Names aren't terminated, but be forgiving if one is anyway. + item.name.resize(strnlen(item.name.c_str(), item.name.size())); + info_.items.push_back(item); + } + + // The package's own PARAM.SFO. There's no reliable pointer to it in the metadata for PSP + // packages (the field is empty), so go by name. + for (const PkgItem &item : info_.items) { + if (item.name != "PARAM.SFO") { + continue; + } + std::vector sfoData; + ParamSFOData sfo; + if (ReadItem(item, &sfoData, 64 * 1024) && sfo.ReadSFO(sfoData)) { + info_.title = sfo.GetValueString("TITLE"); + info_.category = sfo.GetValueString("CATEGORY"); + if (info_.titleId.empty()) { + info_.titleId = sfo.GetValueString("TITLE_ID"); + } + } + break; + } + + for (const PkgItem &item : info_.items) { + if (item.name == "USRDIR/CONTENT/PBOOT.PBP" && !item.IsDirectory()) { + ReadPBOOTInfo(item); + break; + } + } + + INFO_LOG(Log::Loader, "PKG: %s (%s), %d items, update=%d for %s v%s", + info_.contentId.c_str(), info_.category.c_str(), (int)info_.items.size(), + (int)info_.isGameUpdate, info_.discId.c_str(), info_.discVersion.c_str()); + return true; +} + +// The PBOOT is a normal PBP - only its DATA.PSP is encrypted, and its PARAM.SFO is the one that +// says which disc and disc version this patches. That's what makes matching an installed update +// against a game at boot time possible without decrypting anything. +bool PkgReader::ReadPBOOTInfo(const PkgItem &pboot) { + // PBP header: magic, version, then eight little-endian subfile offsets. + u8 header[0x28]; + if (pboot.dataSize < sizeof(header) || !ReadItemData(pboot, 0, sizeof(header), header)) { + return false; + } + if (memcmp(header, "\0PBP", 4) != 0) { + WARN_LOG(Log::Loader, "PKG: PBOOT.PBP isn't a PBP"); + return false; + } + const u32 sfoOffset = Read32LE(header + 0x08); + const u32 iconOffset = Read32LE(header + 0x0C); + if (sfoOffset > iconOffset || iconOffset > pboot.dataSize) { + return false; + } + const u32 sfoSize = iconOffset - sfoOffset; + if (sfoSize == 0 || sfoSize > 64 * 1024) { + return false; + } + + std::vector sfoData(sfoSize); + if (!ReadItemData(pboot, sfoOffset, sfoSize, sfoData.data())) { + return false; + } + ParamSFOData sfo; + if (!sfo.ReadSFO(sfoData)) { + return false; + } + + info_.discId = sfo.GetValueString("DISC_ID"); + info_.discVersion = sfo.GetValueString("DISC_VERSION"); + info_.appVer = sfo.GetValueString("APP_VER"); + info_.systemVer = sfo.GetValueString("PSP_SYSTEM_VER"); + info_.pbootTitle = sfo.GetValueString("PBOOT_TITLE"); + if (info_.title.empty()) { + info_.title = sfo.GetValueString("TITLE"); + } + if (info_.discVersion.empty()) { + info_.discVersion = "1.00"; + } + // A disc ID is what the install is keyed on, so without one there's nowhere to put this. + info_.isGameUpdate = !info_.discId.empty(); + return info_.isGameUpdate; +} + +// Rejects anything that could escape the destination directory. Package filenames are attacker +// data as far as we're concerned. +static bool IsSafeRelativePath(std::string_view name) { + if (name.empty() || name.size() > PKG_MAX_NAME) { + return false; + } + if (name.front() == '/' || name.find('\\') != std::string_view::npos || name.find(':') != std::string_view::npos) { + return false; + } + size_t start = 0; + while (start <= name.size()) { + const size_t slash = name.find('/', start); + const std::string_view part = name.substr(start, slash == std::string_view::npos ? std::string_view::npos : slash - start); + if (part.empty() || part == "." || part == "..") { + return false; + } + for (char c : part) { + // Control characters in a filename are never legitimate here. + if ((unsigned char)c < 0x20) { + return false; + } + } + if (slash == std::string_view::npos) { + break; + } + start = slash + 1; + } + return true; +} + +std::string PkgItemInstallPath(const PkgItem &item) { + // A package wraps its payload PS3-style. USRDIR/CONTENT/ is where the patch's files live, and + // USRDIR/ itself only ever holds ISO.BIN.EDAT; both map to the game folder root. + std::string_view name = item.name; + if (name == "USRDIR" || name == "USRDIR/CONTENT") { + // The wrappers themselves. Both stand for the game folder, which already exists. + return std::string(); + } + if (startsWith(name, "USRDIR/CONTENT/")) { + name = name.substr(strlen("USRDIR/CONTENT/")); + } else if (startsWith(name, "USRDIR/")) { + name = name.substr(strlen("USRDIR/")); + } else { + // Everything at the root is store metadata - PARAM.SFO, PS3LOGO.DAT, ICON0.PNG and + // friends. Installing the SFO would make PPSSPP mistake the folder for save data. + return std::string(); + } + if (name.empty() || !IsSafeRelativePath(name)) { + return std::string(); + } + return std::string(name); +} + +u64 PkgInstalledSize(const PkgInfo &info) { + u64 total = 0; + for (const PkgItem &item : info.items) { + if (!item.IsDirectory() && !PkgItemInstallPath(item).empty()) { + total += item.dataSize; + } + } + return total; +} + +bool InstallPkg(PkgReader &reader, const Path &destDir, const std::function &progress, std::string *error) { + const PkgInfo &info = reader.Info(); + + const u64 totalBytes = PkgInstalledSize(info); + if (totalBytes == 0) { + *error = "Nothing to install in this PKG"; + return false; + } + + if (!File::CreateFullPath(destDir)) { + *error = "Failed to create the destination folder"; + return false; + } + + u64 writtenBytes = 0; + std::vector buffer(PKG_COPY_BLOCK); + + for (const PkgItem &item : info.items) { + const std::string relative = PkgItemInstallPath(item); + if (relative.empty()) { + if (!item.IsDirectory() && !startsWith(item.name, "USRDIR")) { + // Expected - store metadata. Anything else is worth a line in the log. + VERBOSE_LOG(Log::Loader, "PKG: skipping '%s'", item.name.c_str()); + } + continue; + } + const Path destPath = destDir / relative; + if (item.IsDirectory()) { + if (!File::CreateFullPath(destPath)) { + *error = "Failed to create a folder in the destination"; + return false; + } + continue; + } + if (!File::CreateFullPath(destPath.NavigateUp())) { + *error = "Failed to create a folder in the destination"; + return false; + } + + FILE *f = File::OpenCFile(destPath, "wb"); + if (!f) { + *error = "Failed to write to the destination folder"; + return false; + } + + u64 pos = 0; + bool failed = false; + while (pos < item.dataSize) { + const size_t chunk = (size_t)std::min(buffer.size(), item.dataSize - pos); + if (!reader.ReadItemData(item, pos, chunk, buffer.data())) { + *error = "Failed to read from the PKG file"; + failed = true; + break; + } + if (fwrite(buffer.data(), 1, chunk, f) != chunk) { + *error = "Failed to write to the destination folder"; + failed = true; + break; + } + pos += chunk; + writtenBytes += chunk; + if (progress) { + progress((float)((double)writtenBytes / (double)totalBytes)); + } + } + fclose(f); + if (failed) { + File::Delete(destPath); + return false; + } + INFO_LOG(Log::Loader, "PKG: installed %s (%lld bytes)", relative.c_str(), (long long)item.dataSize); + } + + if (progress) { + progress(1.0f); + } + return true; +} diff --git a/Core/Util/PkgUnpack.h b/Core/Util/PkgUnpack.h new file mode 100644 index 0000000000..a13dc6df59 --- /dev/null +++ b/Core/Util/PkgUnpack.h @@ -0,0 +1,108 @@ +// Copyright (c) 2026- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +#pragma once + +#include +#include +#include + +#include "Common/CommonTypes.h" + +class FileLoader; +class Path; + +// Reads .PKG files - the NPDRM container Sony distributed downloadable content in. We're only +// interested in one flavor: PSP *game updates*, which hold a patched EBOOT (PBOOT.PBP) plus the +// data files the patch replaces. Installing one puts them in PSP/GAME//, and the game +// then boots from there with the original disc still supplying everything the patch doesn't +// override - see FindGameUpdatePBOOT() in Core/PSPLoaders.cpp. +// +// The whole package past the header is AES-128-CTR, and every PBOOT seen so far is encrypted with +// a PRX tag we already have a key for, so nothing here needs new crypto secrets. +// +// docs/pkg_notes.md describes the format and what these packages turned out to contain. + +struct PkgItem { + std::string name; + u64 dataOffset = 0; // Relative to the encrypted area, not to the file. + u64 dataSize = 0; + u8 pspType = 0; // 0x90 selects the PSP key, anything else the PS3 key. + u8 flags = 0; // Content type. See docs/pkg_notes.md for the values. + + bool IsDirectory() const { return flags == 4 || flags == 18; } +}; + +struct PkgInfo { + std::string contentId; // "JP0177-ULJM05681_00-PJD2UPDATEVR0101" + std::string titleId; // "ULJM05681" + u32 contentType = 0; // 7 for PSP. See kPkgContentTypePSP. + std::string title; // From the package's own PARAM.SFO. + std::string category; // "PP" for a game update. + + // A game update carries a PBOOT.PBP whose own PARAM.SFO says what it patches. Without one + // there's nothing here we know how to install. + bool isGameUpdate = false; + std::string discId; // The disc this patches, e.g. "ULJM05681". + std::string discVersion; // The disc version it patches, e.g. "1.00". + std::string appVer; // The patch's own version, e.g. "01.01". + std::string systemVer; // Firmware the patch asks for, e.g. "6.20". + std::string pbootTitle; // "Update 2.01", when the patch bothers to name itself. + + std::vector items; +}; + +const u32 kPkgContentTypePSP = 7; + +class PkgReader { +public: + // Parses the header, the item table and the two PARAM.SFOs. Doesn't take ownership of the + // loader, which has to outlive the reader. + bool Open(FileLoader *loader, std::string *error); + + const PkgInfo &Info() const { return info_; } + + // Decrypts `size` bytes at `offset` within an item. + bool ReadItemData(const PkgItem &item, u64 offset, size_t size, u8 *out); + // The whole item, for small ones. Fails rather than allocating more than maxSize. + bool ReadItem(const PkgItem &item, std::vector *out, size_t maxSize = 4 * 1024 * 1024); + +private: + bool ReadEncrypted(u64 offset, size_t size, const u8 *key, u8 *out); + const u8 *ItemKey(const PkgItem &item) const; + bool ReadPBOOTInfo(const PkgItem &pboot); + + FileLoader *loader_ = nullptr; + u64 dataOffset_ = 0; + u64 dataSize_ = 0; + u8 riv_[16]{}; + u8 mainKey_[16]{}; + PkgInfo info_; +}; + +// Where an item ends up inside the installed game folder, or empty for the ones that shouldn't be +// installed at all. A package wraps its payload in USRDIR/CONTENT/, PS3-style, and also carries +// store metadata (PARAM.SFO, PS3LOGO.DAT, the icons) that isn't part of the PSP-side install. +std::string PkgItemInstallPath(const PkgItem &item); + +// What the install will take up on disk. Package contents aren't compressed, so this is exact +// rather than an estimate - modulo the filesystem's own per-file overhead. +u64 PkgInstalledSize(const PkgInfo &info); + +// Unpacks the installable items into destDir, which should be the game folder itself +// (PSP/GAME/). progress is called with 0..1 as it goes, and may be null. +bool InstallPkg(PkgReader &reader, const Path &destDir, const std::function &progress, std::string *error); diff --git a/UWP/CoreUWP/CoreUWP.vcxproj b/UWP/CoreUWP/CoreUWP.vcxproj index 35426278f8..f3e3aab116 100644 --- a/UWP/CoreUWP/CoreUWP.vcxproj +++ b/UWP/CoreUWP/CoreUWP.vcxproj @@ -339,6 +339,7 @@ + @@ -660,6 +661,7 @@ + diff --git a/UWP/CoreUWP/CoreUWP.vcxproj.filters b/UWP/CoreUWP/CoreUWP.vcxproj.filters index 45d4805f88..96576d9464 100644 --- a/UWP/CoreUWP/CoreUWP.vcxproj.filters +++ b/UWP/CoreUWP/CoreUWP.vcxproj.filters @@ -282,6 +282,7 @@ + @@ -673,6 +674,7 @@ + diff --git a/android/jni/Android.mk b/android/jni/Android.mk index 4d3052016c..731940c322 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -814,6 +814,7 @@ EXEC_AND_LIB_FILES := \ $(SRC)/Core/Util/PPGeDraw.cpp \ $(SRC)/Core/Util/KL4E.cpp \ $(SRC)/Core/Util/PSARUnpack.cpp \ + $(SRC)/Core/Util/PkgUnpack.cpp \ $(SRC)/Core/Util/RecentFiles.cpp \ $(SRC)/Core/Util/VideoPlayer.cpp \ $(SRC)/git-version.cpp diff --git a/headless/Headless.cpp b/headless/Headless.cpp index 92dc444804..7bb4b9b326 100644 --- a/headless/Headless.cpp +++ b/headless/Headless.cpp @@ -58,6 +58,7 @@ #include "Core/MIPS/MIPSTables.h" #include "Core/System.h" #include "Core/Util/PSARUnpack.h" +#include "Core/Util/PkgUnpack.h" #include "Core/WebServer.h" #include "Core/HLE/sceUtility.h" #include "Core/SaveState.h" @@ -671,6 +672,38 @@ int main(int argc, const char* argv[]) { return ok ? 0 : 1; } + // Same deal for installing a game update package. + if (cmdLineOptions.installPkg.has_value()) { + if (cmdLineOptions.bootFilenames.size() != 1) { + fprintf(stderr, "--install-pkg takes exactly one .pkg file\n"); + return 1; + } + std::unique_ptr loader(ConstructFileLoader(Path(cmdLineOptions.bootFilenames[0]))); + PkgReader reader; + std::string pkgError; + if (!loader || !reader.Open(loader.get(), &pkgError)) { + fprintf(stderr, "Not a usable PKG: %s\n", pkgError.c_str()); + return 1; + } + const PkgInfo &info = reader.Info(); + printf("%s (%s)\n", info.title.c_str(), info.contentId.c_str()); + printf("Category %s, %d items, %lld bytes installed\n", info.category.c_str(), + (int)info.items.size(), (long long)PkgInstalledSize(info)); + if (info.isGameUpdate) { + printf("Game update for %s v%s -> app version %s (firmware %s)\n", info.discId.c_str(), + info.discVersion.c_str(), info.appVer.c_str(), info.systemVer.c_str()); + } else { + fprintf(stderr, "This PKG isn't a game update - nothing we know how to install\n"); + return 1; + } + if (!InstallPkg(reader, Path(cmdLineOptions.installPkg.value()), nullptr, &pkgError)) { + fprintf(stderr, "Install failed: %s\n", pkgError.c_str()); + return 1; + } + printf("Installed into %s\n", cmdLineOptions.installPkg.value().c_str()); + return 0; + } + g_Config.RestoreDefaults(RestoreSettingsBits::SETTINGS | RestoreSettingsBits::CONTROLS | RestoreSettingsBits::RECENT, false); Core_RegisterDebugOutputListeners(&SendDebugOutput, &SendDebugScreenshot); diff --git a/libretro/Makefile.common b/libretro/Makefile.common index b197b43ffe..c0af28cc5b 100644 --- a/libretro/Makefile.common +++ b/libretro/Makefile.common @@ -902,6 +902,7 @@ SOURCES_CXX += \ $(COREDIR)/Util/PPGeDraw.cpp \ $(COREDIR)/Util/KL4E.cpp \ $(COREDIR)/Util/PSARUnpack.cpp \ + $(COREDIR)/Util/PkgUnpack.cpp \ $(COREDIR)/Util/RecentFiles.cpp \ $(COREDIR)/Util/AudioFormat.cpp \ $(COREDIR)/Util/PathUtil.cpp \