mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-14 23:10:33 +02:00
Texture pack installs took the destination directory from the archive's textures.ini, and PKG update installs from the PBOOT's DISC_ID, both unchecked, so a crafted archive or package could write outside PSP/TEXTURES or PSP/GAME. Add IsSafePathComponent() to PathUtil and check both, replacing the WebServer's own upload filename check with it too. DISC_ID from any PARAM.SFO also names configs, cheats, textures, exdata and savestates, so ReadSFO now drops one that isn't a safe name and the game gets a fake ID like homebrew. The same goes for a frame dump's ID. Combines #22419 and #22420 by Acts1631. Co-Authored-By: Acts1631 <acts1631kjv@proton.me> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
552 lines
18 KiB
C++
552 lines
18 KiB
C++
// 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 <algorithm>
|
|
#include <cstring>
|
|
#include <memory>
|
|
|
|
#include "Common/File/FileUtil.h"
|
|
#include "Common/File/Path.h"
|
|
#include "Common/Log.h"
|
|
#include "Common/StringUtils.h"
|
|
#include "Common/System/Request.h"
|
|
#include "Common/System/System.h"
|
|
#include "Core/ELF/ParamSFO.h"
|
|
#include "Core/ELF/PBPReader.h"
|
|
#include "Core/Loaders.h"
|
|
#include "Core/System.h"
|
|
#include "Core/Util/PathUtil.h"
|
|
#include "Core/Util/PkgUnpack.h"
|
|
|
|
#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<size_t>(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<u8> 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<u8> *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<u8> 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<u8> 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<u8> 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<u8> 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";
|
|
}
|
|
// The install goes in a directory named after the disc ID, so without a usable one there's
|
|
// nowhere to put this.
|
|
info_.isGameUpdate = IsSafePathComponent(info_.discId);
|
|
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<void(float)> &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<u8> 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<u64>(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;
|
|
}
|
|
|
|
bool FindInstalledGameUpdate(std::string_view discId, InstalledGameUpdate *update) {
|
|
if (discId.empty()) {
|
|
return false;
|
|
}
|
|
const Path folder = GetSysDirectory(DIRECTORY_GAME) / std::string(discId);
|
|
const Path pbootPath = folder / "PBOOT.PBP";
|
|
if (!File::Exists(pbootPath)) {
|
|
return false;
|
|
}
|
|
|
|
// The version we want is in the PBOOT's own PARAM.SFO, which isn't encrypted.
|
|
std::unique_ptr<FileLoader> loader(ConstructFileLoader(pbootPath));
|
|
if (!loader) {
|
|
return false;
|
|
}
|
|
PBPReader pbp(loader.get());
|
|
std::vector<u8> sfoData;
|
|
ParamSFOData sfo;
|
|
if (!pbp.IsValid() || !pbp.GetSubFile(PBP_PARAM_SFO, &sfoData) || !sfo.ReadSFO(sfoData)) {
|
|
WARN_LOG(Log::Loader, "'%s' doesn't look like a game update", pbootPath.c_str());
|
|
return false;
|
|
}
|
|
|
|
update->folder = folder;
|
|
update->pbootPath = pbootPath;
|
|
update->appVer = sfo.GetValueString("APP_VER");
|
|
update->discVersion = sfo.GetValueString("DISC_VERSION");
|
|
update->title = sfo.GetValueString("PBOOT_TITLE");
|
|
update->sharesFolderWithGame = File::Exists(folder / "EBOOT.PBP");
|
|
update->sizeOnDisk = update->sharesFolderWithGame
|
|
? (u64)std::max<s64>(0, File::GetFileSize(pbootPath))
|
|
: File::ComputeRecursiveDirectorySize(folder);
|
|
return true;
|
|
}
|
|
|
|
bool DeleteInstalledGameUpdate(const InstalledGameUpdate &update) {
|
|
const bool useTrash = System_GetPropertyBool(SYSPROP_HAS_TRASH_BIN);
|
|
// Only the PBOOT when the folder is a game in its own right - see the struct's comment.
|
|
const Path target = update.sharesFolderWithGame ? update.pbootPath : update.folder;
|
|
INFO_LOG(Log::Loader, "Removing game update '%s'", target.c_str());
|
|
if (useTrash) {
|
|
// TODO: No way to tell whether this succeeded.
|
|
System_MoveToTrash(target);
|
|
return true;
|
|
}
|
|
return update.sharesFolderWithGame ? File::Delete(target) : File::DeleteDirRecursively(target);
|
|
}
|