mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-11-01 03:57:17 +01:00
chdman createcd makes CD CHDs of PSP discs too, which were taken for PS1 games. A CHD is now a PS1 disc only when its volume descriptor's system ID is PLAYSTATION, and otherwise goes on to the PSP disc checks. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
875 lines
31 KiB
C++
875 lines
31 KiB
C++
// Copyright (c) 2012- 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 "Common/File/FileUtil.h"
|
||
#include "Common/File/Path.h"
|
||
#include "Common/StringUtils.h"
|
||
#include "Common/Data/Text/I18n.h"
|
||
#include "Core/FileLoaders/CachingFileLoader.h"
|
||
#include "Core/FileLoaders/DiskCachingFileLoader.h"
|
||
#include "Core/FileLoaders/HTTPFileLoader.h"
|
||
#include "Core/FileLoaders/LocalFileLoader.h"
|
||
#include "Core/FileLoaders/PS1EbootFileLoader.h"
|
||
#include "Core/FileLoaders/RetryingFileLoader.h"
|
||
#include "Core/FileLoaders/ZipFileLoader.h"
|
||
#include "Core/FileSystems/MetaFileSystem.h"
|
||
#include "Core/FileSystems/BlockDevices.h"
|
||
#include "Core/FileSystems/ISOFileSystem.h"
|
||
#include "Core/HLE/sceUmd.h"
|
||
#include "Core/Util/DarwinFileSystemServices.h"
|
||
#include "Core/PSPLoaders.h"
|
||
#include "Core/MemMap.h"
|
||
#include "Core/Loaders.h"
|
||
#include "Core/Core.h"
|
||
#include "Core/System.h"
|
||
#include "Core/ELF/PBPReader.h"
|
||
#include "Core/ELF/ParamSFO.h"
|
||
#include "Core/Util/GameManager.h"
|
||
#include "Core/Util/PathUtil.h"
|
||
|
||
// TODO: This is just part of a VolDescriptor, see ISOFileSystem.cpp.
|
||
struct PVD {
|
||
u8 type;
|
||
char identifier[5];
|
||
char version;
|
||
char pad0;
|
||
char systemId[32]; // PSP GAME normally
|
||
char volumeId[32]; // In PSP games, this sometimes has the name of the game but far from always.
|
||
};
|
||
|
||
FileLoader *ConstructFileLoader(const Path &filename) {
|
||
if (filename.Type() == PathType::HTTP) {
|
||
FileLoader *baseLoader = new RetryingFileLoader(new HTTPFileLoader(filename));
|
||
// For headless, avoid disk caching since it's usually used for tests that might mutate.
|
||
if (!PSP_CoreParameter().headLess) {
|
||
baseLoader = new DiskCachingFileLoader(baseLoader);
|
||
}
|
||
return new CachingFileLoader(baseLoader);
|
||
}
|
||
|
||
return new LocalFileLoader(filename);
|
||
}
|
||
|
||
// TODO : improve, look in the file more
|
||
// Does not take ownership.
|
||
IdentifiedFileType Identify_File(FileLoader *fileLoader, std::string *errorString) {
|
||
errorString->clear();
|
||
if (fileLoader == nullptr) {
|
||
*errorString = "Invalid fileLoader";
|
||
return IdentifiedFileType::ERROR_IDENTIFYING;
|
||
}
|
||
if (fileLoader->GetPath().size() == 0) {
|
||
*errorString = "Invalid filename " + fileLoader->GetPath().ToString();
|
||
return IdentifiedFileType::ERROR_IDENTIFYING;
|
||
}
|
||
|
||
std::string extension = fileLoader->GetFileExtension();
|
||
|
||
// First, check if it's a directory with an EBOOT.PBP in it.
|
||
if (fileLoader->IsDirectory()) {
|
||
Path filename = fileLoader->GetPath();
|
||
if (filename.size() > 4) {
|
||
// Check for existence of EBOOT.PBP, as required for "Directory games".
|
||
if (File::Exists(filename / "EBOOT.PBP")) {
|
||
return IdentifiedFileType::PSP_PBP_DIRECTORY;
|
||
}
|
||
|
||
// check if it's a disc directory
|
||
if (File::Exists(filename / "PSP_GAME")) {
|
||
return IdentifiedFileType::PSP_DISC_DIRECTORY;
|
||
}
|
||
|
||
// Not that, okay, let's guess it's a savedata directory if it has a param.sfo...
|
||
if (File::Exists(filename / "PARAM.SFO")) {
|
||
return IdentifiedFileType::PSP_SAVEDATA_DIRECTORY;
|
||
}
|
||
}
|
||
|
||
return IdentifiedFileType::NORMAL_DIRECTORY;
|
||
}
|
||
|
||
if (!fileLoader->Exists()) {
|
||
*errorString = "IdentifyFile: File doesn't exist: " + fileLoader->GetPath().ToString();
|
||
return IdentifiedFileType::ERROR_IDENTIFYING;
|
||
}
|
||
|
||
if (extension == ".chd" && IsPS1Chd(fileLoader)) {
|
||
return IdentifiedFileType::PSX_ISO;
|
||
}
|
||
|
||
bool isDiscImage = false;
|
||
if (extension == ".iso" || extension == ".cso" || extension == ".chd") {
|
||
isDiscImage = true;
|
||
} else if (extension == ".cue" || extension == ".ccd" || extension == ".mds") {
|
||
// A PS1 disc, which boots as a converted EBOOT (see PS1EbootFileLoader).
|
||
return IsPS1DiscSheet(fileLoader) ? IdentifiedFileType::PSX_ISO : IdentifiedFileType::UNKNOWN;
|
||
} else if (extension == ".mdf") {
|
||
// Read through its .mds, which says how.
|
||
std::unique_ptr<FileLoader> mds(ConstructFileLoader(fileLoader->GetPath().WithReplacedExtension(".mds")));
|
||
return mds && mds->Exists() && IsPS1DiscSheet(mds.get()) ? IdentifiedFileType::PSX_ISO : IdentifiedFileType::UNKNOWN;
|
||
} else if (extension == ".ppst") {
|
||
return IdentifiedFileType::PPSSPP_SAVESTATE;
|
||
} else if (extension == ".ppdmp") {
|
||
char data[8]{};
|
||
fileLoader->ReadAt(0, 8, data);
|
||
if (memcmp(data, "PPSSPPGE", 8) == 0) {
|
||
return IdentifiedFileType::PPSSPP_GE_DUMP;
|
||
}
|
||
}
|
||
|
||
// OK, quick methods of identification for common types failed. Moving on to more expensive methods,
|
||
// starting by reading the first few bytes.
|
||
// This can be necessary for weird Android content storage path types, see issue #17462
|
||
if (isDiscImage || fileLoader->FileSize() >= 0x8800) {
|
||
// Do the quick check for PSP ISOs here.
|
||
std::string bdError;
|
||
std::shared_ptr<BlockDevice> bd(ConstructBlockDevice(fileLoader, &bdError));
|
||
if (bd) {
|
||
u8 block16[2048]{};
|
||
bd->ReadBlock(16, (u8 *)block16);
|
||
PVD pvd;
|
||
memcpy(&pvd, block16, sizeof(PVD));
|
||
if (!memcmp(pvd.identifier, "CD001", 5)) {
|
||
// It's a valid DVD-style ISO file. Let's see which type.
|
||
if (!memcmp(pvd.systemId, "PSP GAME", 8) || !memcmp(pvd.systemId, "\"PSP GAME\"", 10)) {
|
||
// Yes, a known proper PSP game, let's get it going.
|
||
return bd->IsDisc() ? IdentifiedFileType::PSP_ISO : IdentifiedFileType::PSP_ISO_NP;
|
||
} else if (!memcmp(pvd.systemId, "UMD VIDEO", 9) || !memcmp(pvd.systemId, "UMD AUDIO", 9)) {
|
||
// This is rare so being slightly slow here shouldn't be a problem. Let's go check for the presence of
|
||
// actual game data.
|
||
SequentialHandleAllocator hAlloc;
|
||
ISOFileSystem umd(&hAlloc, bd);
|
||
if (DetectUmdDiscType(&umd, "") & PSP_UMD_TYPE_GAME) {
|
||
INFO_LOG(Log::Loader, "Found an UMD VIDEO disc with game data. Treating as game.");
|
||
*errorString = "UMD Video with PSP GAME data";
|
||
return IdentifiedFileType::PSP_ISO;
|
||
}
|
||
|
||
// UMD AUDIO exists technically, but in reality, not really? Let's map it to VIDEO since we support neither.
|
||
return IdentifiedFileType::PSP_UMD_VIDEO_ISO;
|
||
} else if (!memcmp(pvd.systemId, "PS3", 3)) {
|
||
*errorString = "PS3 ISO";
|
||
return IdentifiedFileType::PS3_ISO;
|
||
} else if (!memcmp(pvd.systemId, "PLAYSTATION", 11)) {
|
||
// Just do a size heuristic here to differentiate. There are better ways but slower.
|
||
if (bd->GetUncompressedSize() > 800LL * 1024LL * 1024LL) {
|
||
*errorString = "PS2 ISO";
|
||
return IdentifiedFileType::PS2_ISO;
|
||
}
|
||
*errorString = "PSX ISO?";
|
||
return IdentifiedFileType::PSX_ISO;
|
||
} else {
|
||
// Let's go check for PSP game data.
|
||
SequentialHandleAllocator hAlloc;
|
||
ISOFileSystem umd(&hAlloc, bd);
|
||
if (umd.GetFileInfo("/PSP_GAME").exists) {
|
||
INFO_LOG(Log::Loader, "PSP ISO with unknown system ID: %.32s: %s", pvd.systemId, fileLoader->GetPath().c_str());
|
||
return bd->IsDisc() ? IdentifiedFileType::PSP_ISO : IdentifiedFileType::PSP_ISO_NP;
|
||
}
|
||
|
||
INFO_LOG(Log::Loader, "Unknown ISO with unknown system ID: %.32s: %s", pvd.systemId, fileLoader->GetPath().c_str());
|
||
*errorString = StringFromFormat("ISO with unknown system ID: %.32s", pvd.systemId);
|
||
return IdentifiedFileType::UNKNOWN_ISO;
|
||
}
|
||
}
|
||
|
||
// Do extra check for PSX ISO
|
||
// may be a psx iso, they have 2352 byte sectors. You never know what some people try to open
|
||
if ((fileLoader->FileSize() % 2352) == 0) {
|
||
unsigned char sync[12];
|
||
fileLoader->ReadAt(0, 12, sync);
|
||
|
||
// each sector in a mode2 image starts with these 12 bytes
|
||
if (memcmp(sync, "\x00\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00", 12) == 0) {
|
||
*errorString = "ISO is a CD - likely PSX"; // Mode 2 CDs are used for PSX games
|
||
return IdentifiedFileType::PSX_ISO;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (isDiscImage) {
|
||
if (!bdError.empty()) {
|
||
*errorString = bdError;
|
||
} else {
|
||
auto sy = GetI18NCategory(I18NCat::SYSTEM);
|
||
*errorString = sy->T("Not a PSP game");
|
||
}
|
||
return IdentifiedFileType::UNKNOWN_ISO;
|
||
}
|
||
}
|
||
|
||
u32 id;
|
||
|
||
size_t readSize = fileLoader->ReadAt(0, 4, 1, &id);
|
||
if (readSize != 1) {
|
||
*errorString = "Failed to read identification bytes";
|
||
return IdentifiedFileType::ERROR_IDENTIFYING;
|
||
}
|
||
|
||
u32_le psar_offset = 0, psar_id = 0;
|
||
if (!memcmp(&id, "PK\x03\x04", 4) || !memcmp(&id, "PK\x05\x06", 4) || !memcmp(&id, "PK\x07\x08", 4)) {
|
||
return IdentifiedFileType::ARCHIVE_ZIP;
|
||
} else if (!memcmp(&id, "\x00PBP", 4)) {
|
||
fileLoader->ReadAt(0x24, 4, 1, &psar_offset);
|
||
fileLoader->ReadAt(psar_offset, 4, 1, &psar_id);
|
||
// Fall through to the below if chain.
|
||
} else if (!memcmp(&id, "Rar!", 4)) {
|
||
return IdentifiedFileType::ARCHIVE_RAR;
|
||
} else if (!memcmp(&id, "\x37\x7A\xBC\xAF", 4)) {
|
||
return IdentifiedFileType::ARCHIVE_7Z;
|
||
} else if (!memcmp(&id, "\x7F""PKG", 4)) {
|
||
return IdentifiedFileType::PSP_PKG;
|
||
}
|
||
|
||
// "~PSP" is an encrypted PRX. The module loader decrypts those on the way in, so as far as
|
||
// identification goes it's the same thing as a plain ELF - which is how the modules in a
|
||
// firmware unpacked straight from an updater arrive (see Core/Util/PSARUnpack.cpp).
|
||
if (id == 'FLE\x7F' || id == 'PSP~') {
|
||
Path filename = fileLoader->GetPath();
|
||
// There are a few elfs misnamed as pbp (like Trig Wars), accept that. Also accept extension-less paths.
|
||
if (extension == ".plf" || strstr(filename.GetFilename().c_str(), "BOOT.BIN") ||
|
||
extension == ".elf" || extension == ".prx" || extension == ".pbp" || extension.empty()) {
|
||
return IdentifiedFileType::PSP_ELF;
|
||
}
|
||
return IdentifiedFileType::UNKNOWN_ELF;
|
||
} else if (id == 'PBP\x00') {
|
||
// Do this PS1 eboot check FIRST before checking other eboot types.
|
||
// It seems like some are malformed and slip through the PSAR check below.
|
||
PBPReader pbp(fileLoader);
|
||
if (pbp.IsValid() && !pbp.IsELF()) {
|
||
std::vector<u8> sfoData;
|
||
if (pbp.GetSubFile(PBP_PARAM_SFO, &sfoData)) {
|
||
ParamSFOData paramSFO;
|
||
paramSFO.ReadSFO(sfoData);
|
||
// PS1 Eboots are supposed to use "ME" as their PARAM SFO category.
|
||
// If they don't, and they're still malformed (e.g. PSISOIMG0000 isn't found), there's nothing we can do.
|
||
if (paramSFO.GetValueString("CATEGORY") == "ME") {
|
||
return IdentifiedFileType::PSP_PS1_PBP;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (psar_id == 'MUPN') {
|
||
return IdentifiedFileType::PSP_ISO_NP;
|
||
}
|
||
// PS1 PSAR begins with "PSISOIMG0000"
|
||
if (psar_id == 'SISP') {
|
||
return IdentifiedFileType::PSP_PS1_PBP;
|
||
}
|
||
|
||
// Let's check if we got pointed to a PBP within such a directory.
|
||
// If so we just move up and return the directory itself as the game.
|
||
// If loading from memstick...
|
||
if (fileLoader->GetPath().FilePathContainsNoCase("PSP/GAME/")) {
|
||
return IdentifiedFileType::PSP_PBP_DIRECTORY;
|
||
}
|
||
return IdentifiedFileType::PSP_PBP;
|
||
} else if (extension == ".pbp") {
|
||
ERROR_LOG(Log::Loader, "A PBP with the wrong magic number?");
|
||
return IdentifiedFileType::PSP_PBP;
|
||
} else if (extension == ".bin") {
|
||
return IdentifiedFileType::UNKNOWN_BIN;
|
||
} else if (extension == ".zip") {
|
||
return IdentifiedFileType::ARCHIVE_ZIP;
|
||
} else if (extension == ".rar") {
|
||
return IdentifiedFileType::ARCHIVE_RAR;
|
||
} else if (extension == ".r00") {
|
||
return IdentifiedFileType::ARCHIVE_RAR;
|
||
} else if (extension == ".r01") {
|
||
return IdentifiedFileType::ARCHIVE_RAR;
|
||
} else if (extension == ".7z") {
|
||
return IdentifiedFileType::ARCHIVE_7Z;
|
||
} else if (extension == ".pkg") {
|
||
// Magic didn't match, but the name says what it was meant to be - report it as a PKG so
|
||
// the install screen can explain what's wrong with it.
|
||
return IdentifiedFileType::PSP_PKG;
|
||
}
|
||
return IdentifiedFileType::UNKNOWN;
|
||
}
|
||
|
||
FileLoader *ResolveFileLoaderTarget(FileLoader *fileLoader, IdentifiedFileType *fileType, std::string *errorString) {
|
||
*fileType = Identify_File(fileLoader, errorString);
|
||
if (*fileType == IdentifiedFileType::PSP_PBP_DIRECTORY) {
|
||
const Path ebootFilename = ResolvePBPFile(fileLoader->GetPath());
|
||
if (ebootFilename != fileLoader->GetPath()) {
|
||
// Switch fileLoader to the actual EBOOT.
|
||
delete fileLoader;
|
||
fileLoader = ConstructFileLoader(ebootFilename);
|
||
// Re-identify the file.
|
||
*fileType = Identify_File(fileLoader, errorString);
|
||
}
|
||
} else if (*fileType == IdentifiedFileType::PSX_ISO) {
|
||
// A PS1 disc image runs on the firmware's POPS, as a converted EBOOT made from it.
|
||
PS1EbootFileLoader *eboot = new PS1EbootFileLoader(fileLoader->GetPath());
|
||
if (!eboot->IsValid()) {
|
||
*errorString = eboot->LatestError();
|
||
delete eboot;
|
||
return fileLoader;
|
||
}
|
||
delete fileLoader;
|
||
*fileType = IdentifiedFileType::PSP_PS1_PBP;
|
||
return eboot;
|
||
} else if (*fileType == IdentifiedFileType::ARCHIVE_ZIP) {
|
||
// Handle zip files, take automatic action depending on contents.
|
||
// Can also return nullptr.
|
||
ZipFileLoader *zipLoader = new ZipFileLoader(fileLoader);
|
||
|
||
ZipFileInfo zipFileInfo{};
|
||
DetectZipFileContents(zipLoader->GetZip(), &zipFileInfo);
|
||
|
||
switch (zipFileInfo.contents) {
|
||
case ZipFileContents::ISO_FILE:
|
||
case ZipFileContents::FRAME_DUMP:
|
||
{
|
||
zipLoader->Initialize(zipFileInfo.isoFileIndex);
|
||
// Re-identify the file.
|
||
*fileType = Identify_File(zipLoader, errorString);
|
||
return zipLoader;
|
||
}
|
||
default:
|
||
{
|
||
// Nothing runnable in file. Take the original loader back and return it.
|
||
fileLoader = zipLoader->Steal();
|
||
delete zipLoader;
|
||
return fileLoader;
|
||
}
|
||
}
|
||
}
|
||
return fileLoader;
|
||
}
|
||
|
||
Path ResolvePBPDirectory(const Path &filename) {
|
||
if (filename.GetFilename() == "EBOOT.PBP") {
|
||
return filename.NavigateUp();
|
||
} else {
|
||
return filename;
|
||
}
|
||
}
|
||
|
||
Path ResolvePBPFile(const Path &filename) {
|
||
if (filename.GetFilename() != "EBOOT.PBP") {
|
||
return filename / "EBOOT.PBP";
|
||
} else {
|
||
return filename;
|
||
}
|
||
}
|
||
|
||
bool UmdReplace(const Path &filepath, FileLoader **fileLoader, std::string &error) {
|
||
IFileSystem *currentUMD = pspFileSystem.GetSystem("disc0:");
|
||
|
||
if (!currentUMD) {
|
||
error = "has no disc";
|
||
return false;
|
||
}
|
||
return UmdMount(filepath, fileLoader, error);
|
||
}
|
||
|
||
bool UmdMount(const Path &filepath, FileLoader **fileLoader, std::string &error) {
|
||
FileLoader *loadedFile = ConstructFileLoader(filepath);
|
||
|
||
if (!loadedFile || !loadedFile->Exists()) {
|
||
error = loadedFile ? (GetFriendlyPath(loadedFile->GetPath()) + " doesn't exist") : "no loaded file";
|
||
delete loadedFile;
|
||
return false;
|
||
}
|
||
UpdateLoadedFile(loadedFile);
|
||
|
||
std::string errorString;
|
||
IdentifiedFileType fileType;
|
||
loadedFile = ResolveFileLoaderTarget(loadedFile, &fileType, &errorString);
|
||
|
||
*fileLoader = loadedFile;
|
||
|
||
switch (fileType) {
|
||
case IdentifiedFileType::PSP_ISO:
|
||
case IdentifiedFileType::PSP_ISO_NP:
|
||
case IdentifiedFileType::PSP_DISC_DIRECTORY:
|
||
case IdentifiedFileType::PSP_UMD_VIDEO_ISO:
|
||
if (!MountGameISO(loadedFile, &error)) {
|
||
error = "mounting the ISO failed: " + error;
|
||
return false;
|
||
}
|
||
break;
|
||
default:
|
||
error = "Unsupported file type: " + std::string(IdentifiedFileTypeToString(fileType)) + " " + errorString;
|
||
return false;
|
||
break;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
u32 DetectUmdDiscType(IFileSystem *fs, std::string_view root) {
|
||
const auto exists = [&](std::string_view path) {
|
||
return fs->GetFileInfo(join(root, path)).exists;
|
||
};
|
||
|
||
u32 type = 0;
|
||
// Pressed discs carry UMD_DATA.BIN, "ULUS-10041|06B7CAAD47576766|0001|G", whose fourth field
|
||
// lists the content: G for a game, V for video, A for audio. The EUR Video Demo Disc says GVA.
|
||
const std::string umdDataPath = join(root, "/UMD_DATA.BIN");
|
||
if (fs->GetFileInfo(umdDataPath).exists) {
|
||
const int handle = fs->OpenFile(umdDataPath, FILEACCESS_READ);
|
||
if (handle >= 0) {
|
||
char data[128];
|
||
const size_t size = fs->ReadFile(handle, (u8 *)data, sizeof(data));
|
||
fs->CloseFile(handle);
|
||
std::vector<std::string_view> fields;
|
||
SplitString(std::string_view(data, size), '|', fields);
|
||
if (fields.size() >= 4) {
|
||
for (char c : fields[3]) {
|
||
switch (c) {
|
||
case 'G': type |= PSP_UMD_TYPE_GAME; break;
|
||
case 'V': type |= PSP_UMD_TYPE_VIDEO; break;
|
||
case 'A': type |= PSP_UMD_TYPE_AUDIO; break;
|
||
default: break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (type == 0) {
|
||
// Homebrew and patched images often have no UMD_DATA.BIN.
|
||
if (exists("/PSP_GAME")) {
|
||
type |= PSP_UMD_TYPE_GAME;
|
||
}
|
||
if (exists("/UMD_VIDEO")) {
|
||
type |= PSP_UMD_TYPE_VIDEO;
|
||
}
|
||
if (exists("/UMD_AUDIO")) {
|
||
type |= PSP_UMD_TYPE_AUDIO;
|
||
}
|
||
}
|
||
// Some video discs have a PSP_GAME holding nothing but a firmware update (SYSDIR/UPDATE), and
|
||
// the VSH refuses such a disc if we call it a game too (#22737).
|
||
if ((type & PSP_UMD_TYPE_GAME) && !exists("/PSP_GAME/SYSDIR/EBOOT.BIN") && (type & ~PSP_UMD_TYPE_GAME) != 0) {
|
||
type &= ~PSP_UMD_TYPE_GAME;
|
||
}
|
||
return type;
|
||
}
|
||
|
||
u32 DetectUmdDiscType(FileLoader *fileLoader) {
|
||
std::string error;
|
||
std::shared_ptr<BlockDevice> bd(ConstructBlockDevice(fileLoader, &error));
|
||
if (!bd) {
|
||
return 0;
|
||
}
|
||
SequentialHandleAllocator handles;
|
||
ISOFileSystem umd(&handles, bd);
|
||
return DetectUmdDiscType(&umd, "");
|
||
}
|
||
|
||
bool UmdHasBootableGame(FileLoader *fileLoader) {
|
||
std::string error;
|
||
std::shared_ptr<BlockDevice> bd(ConstructBlockDevice(fileLoader, &error));
|
||
if (!bd) {
|
||
return false;
|
||
}
|
||
SequentialHandleAllocator handles;
|
||
ISOFileSystem umd(&handles, bd);
|
||
for (const char *path : { "/PSP_GAME/SYSDIR/EBOOT.BIN", "/PSP_GAME/SYSDIR/BOOT.BIN" }) {
|
||
if (!umd.GetFileInfo(path).exists) {
|
||
continue;
|
||
}
|
||
const int handle = umd.OpenFile(path, FILEACCESS_READ);
|
||
if (handle < 0) {
|
||
continue;
|
||
}
|
||
u8 head[4]{};
|
||
const size_t read = umd.ReadFile(handle, head, sizeof(head));
|
||
umd.CloseFile(handle);
|
||
if (read == sizeof(head) && (memcmp(head, "~PSP", 4) == 0 || memcmp(head, "\x7F" "ELF", 4) == 0)) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// Close the return value with ZipClose (if non-null, of course).
|
||
ZipContainer ZipOpenPath(const Path &fileName) {
|
||
ZipContainer z(fileName);
|
||
if (z == nullptr) {
|
||
ERROR_LOG(Log::HLE, "Failed to open ZIP file '%s'", fileName.c_str());
|
||
}
|
||
return z;
|
||
}
|
||
|
||
void ZipClose(ZipContainer &z) {
|
||
z.close();
|
||
}
|
||
|
||
bool DetectZipFileContents(const Path &fileName, ZipFileInfo *info) {
|
||
ZipContainer z = ZipOpenPath(fileName);
|
||
if (!z) {
|
||
info->contents = ZipFileContents::UNKNOWN;
|
||
return false;
|
||
}
|
||
DetectZipFileContents(z, info);
|
||
return true;
|
||
}
|
||
|
||
static int countSlashes(const std::string &fileName, int *slashLocation) {
|
||
int slashCount = 0;
|
||
int lastSlashLocation = -1;
|
||
if (slashLocation) {
|
||
*slashLocation = -1;
|
||
}
|
||
for (size_t i = 0; i < fileName.size(); i++) {
|
||
if (fileName[i] == '/') {
|
||
slashCount++;
|
||
if (slashLocation) {
|
||
*slashLocation = lastSlashLocation;
|
||
lastSlashLocation = (int)i;
|
||
}
|
||
}
|
||
}
|
||
|
||
return slashCount;
|
||
}
|
||
|
||
inline char asciitolower(char in) {
|
||
if (in <= 'Z' && in >= 'A')
|
||
return in - ('Z' - 'z');
|
||
return in;
|
||
}
|
||
|
||
// Only used for small metadata files (PARAM.SFO, plugin ini). The size below is far above
|
||
// anything legitimate - it exists because the size comes from the zip's central directory,
|
||
// i.e. straight from an untrusted file.
|
||
static const u64 MAX_ZIP_EXTRACT_TO_MEMORY_SIZE = 16 * 1024 * 1024;
|
||
|
||
static bool ZipExtractFileToMemory(struct zip *z, int fileIndex, std::string *data) {
|
||
zip_stat_t zstat{};
|
||
if (zip_stat_index(z, fileIndex, 0, &zstat) != 0) {
|
||
ERROR_LOG(Log::HLE, "zip_stat_index failed for file %d in zip", fileIndex);
|
||
return false;
|
||
}
|
||
if (!(zstat.valid & ZIP_STAT_SIZE)) {
|
||
ERROR_LOG(Log::HLE, "No size for file %d in zip", fileIndex);
|
||
return false;
|
||
}
|
||
if (zstat.size == 0) {
|
||
data->clear();
|
||
return true;
|
||
}
|
||
if (zstat.size > MAX_ZIP_EXTRACT_TO_MEMORY_SIZE) {
|
||
ERROR_LOG(Log::HLE, "Refusing to extract file %d from zip: declared size %llu is implausible", fileIndex, (unsigned long long)zstat.size);
|
||
return false;
|
||
}
|
||
|
||
size_t readSize = (size_t)zstat.size;
|
||
data->resize(readSize);
|
||
|
||
zip_file *zf = zip_fopen_index(z, fileIndex, 0);
|
||
if (!zf) {
|
||
ERROR_LOG(Log::HLE, "Failed to zip_fopen_index file %d from zip", fileIndex);
|
||
return false;
|
||
}
|
||
|
||
zip_int64_t retval = zip_fread(zf, data->data(), readSize);
|
||
zip_fclose(zf);
|
||
|
||
if (retval < 0 || retval < (int)readSize) {
|
||
ERROR_LOG(Log::HLE, "Failed to read %d bytes from zip (%d) - archive corrupt?", (int)readSize, (int)retval);
|
||
return false;
|
||
} else {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// TODO: Make this generic and handle all the things.
|
||
// For now, it'll just handle unpacking ISOs.
|
||
bool DetectArchiveContents(VFSInterface *vfs, ZipFileInfo *info) {
|
||
info->archiveType = ArchiveType::SevenZ;
|
||
info->contents = ZipFileContents::UNKNOWN;
|
||
std::vector<File::FileInfo> listing;
|
||
vfs->GetFileListing("", &listing, nullptr);
|
||
for (auto &file : listing) {
|
||
std::string ext = file.fullName.GetFileExtension(); // always lowercase
|
||
if (ext == ".iso" || ext == ".cso") {
|
||
info->contents = ZipFileContents::ISO_FILE;
|
||
info->isoFilename = file.fullName.ToString();
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// A PS1 disc in the root or one level down, as in a zip.
|
||
std::vector<ArchiveEntry> entries;
|
||
for (const File::FileInfo &file : listing) {
|
||
if (!file.isDirectory) {
|
||
entries.push_back(ArchiveEntry{ file.fullName.ToString(), (s64)file.size });
|
||
continue;
|
||
}
|
||
std::vector<File::FileInfo> subListing;
|
||
vfs->GetFileListing(file.fullName.ToString(), &subListing, nullptr);
|
||
for (const File::FileInfo &sub : subListing) {
|
||
if (!sub.isDirectory) {
|
||
entries.push_back(ArchiveEntry{ sub.fullName.ToString(), (s64)sub.size });
|
||
}
|
||
}
|
||
}
|
||
if (FindPS1DiscFiles(entries, [vfs](const std::string &name, std::string *text) {
|
||
size_t size = 0;
|
||
uint8_t *data = vfs->ReadFile(name, &size);
|
||
if (!data || size > 64 * 1024) {
|
||
delete[] data;
|
||
return false;
|
||
}
|
||
text->assign((const char *)data, size);
|
||
delete[] data;
|
||
return true;
|
||
}, &info->discFiles)) {
|
||
info->contents = ZipFileContents::PSX_DISC;
|
||
info->contentName = info->discFiles[0];
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
void DetectZipFileContents(zip_t *z, ZipFileInfo *info) {
|
||
info->archiveType = ArchiveType::ZIP;
|
||
int numFiles = zip_get_num_files(z);
|
||
if (numFiles < 0) {
|
||
// Broken zip archive?
|
||
ERROR_LOG(Log::HLE, "Failed to get the file count of zip file");
|
||
info->contents = ZipFileContents::UNKNOWN;
|
||
return;
|
||
}
|
||
_dbg_assert_(numFiles >= 0);
|
||
|
||
// Verify that this is a PSP zip file with the correct layout. We also try
|
||
// to detect simple zipped ISO files, those we'll just "install" to the current
|
||
// directory of the Games tab (where else?).
|
||
bool isPSPMemstickGame = false;
|
||
bool isZippedISO = false;
|
||
bool isTexturePack = false;
|
||
bool isSaveStates = false;
|
||
bool isFrameDump = false;
|
||
int stripChars = 0;
|
||
int stripCharsTexturePack = -1;
|
||
int filesInRoot = 0;
|
||
int filesInSubdirs = 0;
|
||
int directoriesInRoot = 0;
|
||
std::string rootSaveDir; // SAVEDATA_DIRECTORY of a PARAM.SFO in the root of the zip.
|
||
bool hasParamSFO = false;
|
||
bool isExtractedISO = false;
|
||
bool hasIcon0PNG = false;
|
||
bool hasPRX = false;
|
||
bool hasPluginIni = false;
|
||
s64 totalFileSize = 0;
|
||
std::vector<ArchiveEntry> archiveEntries; // In the root or one level down, where a disc image can be.
|
||
|
||
// TODO: It might be cleaner to write separate detection functions, but this big loop doing it all at once
|
||
// is quite convenient and makes it easy to add shared heuristics.
|
||
for (int i = 0; i < numFiles; i++) {
|
||
const char *fn = zip_get_name(z, i, 0);
|
||
|
||
zip_stat_t stat{};
|
||
zip_stat_index(z, i, 0, &stat);
|
||
totalFileSize += stat.size;
|
||
|
||
std::string fileName(fn);
|
||
if (endsWith(fileName, "/")) {
|
||
// A directory. Not all zips bother including these.
|
||
continue;
|
||
}
|
||
|
||
std::string lowerCaseName = fileName; // actually, lowercase-name
|
||
std::transform(lowerCaseName.begin(), lowerCaseName.end(), lowerCaseName.begin(),
|
||
[](unsigned char c) { return asciitolower(c); }); // Not using std::tolower to avoid Turkish I->ı conversion.
|
||
// Ignore macos metadata stuff
|
||
if (startsWith(lowerCaseName, "__macosx/")) {
|
||
continue;
|
||
}
|
||
|
||
int prevSlashLocation = -1;
|
||
int slashCount = countSlashes(lowerCaseName, &prevSlashLocation);
|
||
if (slashCount <= 1) {
|
||
archiveEntries.push_back(ArchiveEntry{ fileName, (s64)stat.size });
|
||
}
|
||
if (lowerCaseName.find("eboot.pbp") != std::string::npos) {
|
||
if (slashCount >= 1 && (!isPSPMemstickGame || prevSlashLocation < stripChars + 1)) {
|
||
stripChars = prevSlashLocation + 1;
|
||
isPSPMemstickGame = true;
|
||
} else {
|
||
INFO_LOG(Log::HLE, "Wrong number of slashes (%i) in '%s'", slashCount, fn);
|
||
}
|
||
// TODO: Extract icon and param.sfo from the pbp to be able to display it on the install screen.
|
||
} else if (endsWith(lowerCaseName, ".iso") || endsWith(lowerCaseName, ".cso") || endsWith(lowerCaseName, ".chd")) {
|
||
if (slashCount <= 1) {
|
||
// We only do this if the ISO file is in the root or one level down.
|
||
isZippedISO = true;
|
||
INFO_LOG(Log::HLE, "ISO found in zip: %s", lowerCaseName.c_str());
|
||
if (info->isoFileIndex != -1) {
|
||
INFO_LOG(Log::HLE, "More than one ISO file found in zip. Ignoring additional ones.");
|
||
} else {
|
||
info->isoFileIndex = i;
|
||
info->contentName = fn;
|
||
}
|
||
}
|
||
} else if (lowerCaseName.find("textures.ini") != std::string::npos) {
|
||
int slashLocation = (int)lowerCaseName.find_last_of('/');
|
||
if (stripCharsTexturePack == -1 || slashLocation < stripCharsTexturePack + 1) {
|
||
stripCharsTexturePack = slashLocation + 1;
|
||
isTexturePack = true;
|
||
info->textureIniIndex = i;
|
||
}
|
||
} else if (endsWith(lowerCaseName, ".ppdmp")) {
|
||
isFrameDump = true;
|
||
info->isoFileIndex = i;
|
||
info->contentName = fn;
|
||
} else if (endsWith(lowerCaseName, ".ppst")) {
|
||
int slashLocation = (int)lowerCaseName.find_last_of('/');
|
||
if (stripChars == 0 || slashLocation < stripChars + 1) {
|
||
stripChars = slashLocation + 1;
|
||
}
|
||
isSaveStates = true;
|
||
info->gameTitle = fn;
|
||
} else if (endsWith(lowerCaseName, "psp_game/sysdir/eboot.bin") || endsWith(lowerCaseName, "psp_game/sysdir/boot.bin")) {
|
||
isExtractedISO = true;
|
||
} else if (lowerCaseName == "param.sfo" || endsWith(lowerCaseName, "/param.sfo")) {
|
||
// Get the game name so we can display it.
|
||
std::string paramSFOContents;
|
||
if (ZipExtractFileToMemory(z, i, ¶mSFOContents)) {
|
||
ParamSFOData sfo;
|
||
if (sfo.ReadSFO((const u8 *)paramSFOContents.data(), paramSFOContents.size())) {
|
||
if (sfo.HasKey("TITLE")) {
|
||
info->gameTitle = sfo.GetValueString("TITLE");
|
||
info->savedataTitle = sfo.GetValueString("SAVEDATA_TITLE");
|
||
char buff[20];
|
||
strftime(buff, 20, "%Y-%m-%d %H:%M:%S", localtime(&stat.mtime));
|
||
info->mTime = buff;
|
||
info->savedataDetails = sfo.GetValueString("SAVEDATA_DETAIL");
|
||
info->savedataDir = sfo.GetValueString("SAVEDATA_DIRECTORY"); // should also be parsable from the path.
|
||
hasParamSFO = true;
|
||
if (slashCount == 0) {
|
||
rootSaveDir = info->savedataDir;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} else if (endsWith(lowerCaseName, "/icon0.png")) {
|
||
hasIcon0PNG = true;
|
||
} else if (endsWith(lowerCaseName, "/plugin.ini") && slashCount >= 1) {
|
||
const size_t slashLocation = lowerCaseName.find_last_of('/'); // can't miss, we have a slash in the endsWith check.
|
||
// Find previous slash to determine the root of the plugin, so we can display it properly.
|
||
if (slashLocation > 0) {
|
||
int prevSlashLocation = (int)lowerCaseName.find_last_of('/', slashLocation - 1);
|
||
if (prevSlashLocation == std::string::npos) {
|
||
stripChars = 0;
|
||
} else {
|
||
stripChars = prevSlashLocation + 1;
|
||
}
|
||
} else {
|
||
// Can't load plugins where plugin.ini is in the root.
|
||
continue;
|
||
}
|
||
hasPluginIni = true;
|
||
ZipExtractFileToMemory(z, i, &info->iniContents);
|
||
info->contentName = fileName.substr(0, fileName.find_last_of('/'));
|
||
} else if (endsWith(lowerCaseName, ".prx")) {
|
||
hasPRX = true;
|
||
}
|
||
if (slashCount == 0) {
|
||
filesInRoot++;
|
||
} else {
|
||
filesInSubdirs++;
|
||
}
|
||
}
|
||
|
||
info->stripChars = stripChars;
|
||
info->numFiles = numFiles;
|
||
info->ignoreMetaFiles = false;
|
||
info->totalFileSize = totalFileSize;
|
||
|
||
// A PS1 disc: a cue sheet, .ccd or .mds and its files, or a lone raw image, which only counts if nothing else does.
|
||
const bool isPS1Disc = FindPS1DiscFiles(archiveEntries, [z](const std::string &name, std::string *text) {
|
||
const zip_int64_t index = zip_name_locate(z, name.c_str(), 0);
|
||
zip_stat_t stat{};
|
||
return index >= 0 && zip_stat_index(z, index, 0, &stat) >= 0 && stat.size < 64 * 1024 && ZipExtractFileToMemory(z, (int)index, text);
|
||
}, &info->discFiles);
|
||
const std::string discExt = isPS1Disc ? Path(info->discFiles[0]).GetFileExtension() : "";
|
||
const bool isPS1DiscWithCue = discExt == ".cue" || discExt == ".ccd" || discExt == ".mds";
|
||
|
||
// Priority ordering for detecting the various kinds of zip file content.s
|
||
if (isPSPMemstickGame) {
|
||
info->contents = ZipFileContents::PSP_GAME_DIR;
|
||
} else if (isZippedISO) {
|
||
info->contents = ZipFileContents::ISO_FILE;
|
||
} else if (isPS1DiscWithCue) {
|
||
info->contents = ZipFileContents::PSX_DISC;
|
||
info->contentName = info->discFiles[0];
|
||
} else if (isTexturePack) {
|
||
info->stripChars = stripCharsTexturePack;
|
||
info->ignoreMetaFiles = true;
|
||
info->contents = ZipFileContents::TEXTURE_PACK;
|
||
} else if (stripChars == 0 && filesInRoot == 0 && hasParamSFO && hasIcon0PNG && !isExtractedISO) {
|
||
// As downloaded from GameFAQs, for example.
|
||
info->contents = ZipFileContents::SAVE_DATA;
|
||
} else if (filesInSubdirs == 0 && IsSafePathComponent(rootSaveDir) && !isExtractedISO && !isSaveStates && !isFrameDump) {
|
||
// The contents of a single save folder, zipped without the folder itself (the zip is usually named
|
||
// after it). The folder name is the SFO's SAVEDATA_DIRECTORY (game ID + save name, like ULUS10019000).
|
||
info->contents = ZipFileContents::SAVE_DATA;
|
||
info->saveDataInRoot = true;
|
||
info->savedataDir = rootSaveDir;
|
||
} else if (isFrameDump) {
|
||
info->contents = ZipFileContents::FRAME_DUMP;
|
||
} else if (isSaveStates) {
|
||
info->contents = ZipFileContents::SAVE_STATES;
|
||
} else if (isExtractedISO && hasParamSFO) {
|
||
info->contents = ZipFileContents::EXTRACTED_GAME;
|
||
} else if (hasPluginIni && hasPRX) {
|
||
info->contents = ZipFileContents::PRX_PLUGIN;
|
||
} else if (isPS1Disc) {
|
||
info->contents = ZipFileContents::PSX_DISC;
|
||
info->contentName = info->discFiles[0];
|
||
} else {
|
||
info->contents = ZipFileContents::UNKNOWN;
|
||
}
|
||
}
|
||
|
||
const char *IdentifiedFileTypeToString(IdentifiedFileType type) {
|
||
switch (type) {
|
||
case IdentifiedFileType::ERROR_IDENTIFYING: return "ERROR_IDENTIFYING";
|
||
case IdentifiedFileType::PSP_PBP_DIRECTORY: return "PSP_PBP_DIRECTORY";
|
||
case IdentifiedFileType::PSP_PBP: return "PSP_PBP";
|
||
case IdentifiedFileType::PSP_ELF: return "PSP_ELF";
|
||
case IdentifiedFileType::PSP_ISO: return "PSP_ISO";
|
||
case IdentifiedFileType::PSP_ISO_NP: return "PSP_ISO_NP";
|
||
case IdentifiedFileType::PSP_DISC_DIRECTORY: return "PSP_DISC_DIRECTORY";
|
||
case IdentifiedFileType::UNKNOWN_BIN: return "UNKNOWN_BIN";
|
||
case IdentifiedFileType::UNKNOWN_ELF: return "UNKNOWN_ELF";
|
||
case IdentifiedFileType::UNKNOWN_ISO: return "UNKNOWN_ISO";
|
||
case IdentifiedFileType::ARCHIVE_RAR: return "ARCHIVE_RAR";
|
||
case IdentifiedFileType::ARCHIVE_ZIP: return "ARCHIVE_ZIP";
|
||
case IdentifiedFileType::PSP_PKG: return "PSP_PKG";
|
||
case IdentifiedFileType::ARCHIVE_7Z: return "ARCHIVE_7Z";
|
||
case IdentifiedFileType::PSP_PS1_PBP: return "PSP_PS1_PBP";
|
||
case IdentifiedFileType::PSX_ISO: return "PSX_ISO";
|
||
case IdentifiedFileType::PS2_ISO: return "PS2_ISO";
|
||
case IdentifiedFileType::PS3_ISO: return "PS3_ISO";
|
||
case IdentifiedFileType::PSP_UMD_VIDEO_ISO: return "UMD_VIDEO";
|
||
case IdentifiedFileType::NORMAL_DIRECTORY: return "NORMAL_DIRECTORY";
|
||
case IdentifiedFileType::PSP_SAVEDATA_DIRECTORY: return "PSP_SAVEDATA_DIRECTORY";
|
||
case IdentifiedFileType::PPSSPP_SAVESTATE: return "PPSSPP_SAVESTATE";
|
||
case IdentifiedFileType::PPSSPP_GE_DUMP: return "PPSSPP_GE_DUMP";
|
||
case IdentifiedFileType::UNKNOWN: return "UNKNOWN";
|
||
default: return "INVALID_TYPE";
|
||
}
|
||
}
|