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 \