diff --git a/Core/FileLoaders/PS1EbootFileLoader.cpp b/Core/FileLoaders/PS1EbootFileLoader.cpp index 2702737849..08bb50c2bb 100644 --- a/Core/FileLoaders/PS1EbootFileLoader.cpp +++ b/Core/FileLoaders/PS1EbootFileLoader.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include "Common/File/DirListing.h" @@ -148,11 +149,15 @@ bool IsPS1CueOrCcd(FileLoader *fileLoader) { } std::vector CueSheetFileNames(const Path &cuePath) { - std::vector names; std::string text; if (!ReadSmallTextFile(cuePath, &text)) { - return names; + return {}; } + return CueSheetFileNamesFromText(text); +} + +std::vector CueSheetFileNamesFromText(std::string_view text) { + std::vector names; std::vector lines; SplitString(text, '\n', lines); for (std::string_view line : lines) { @@ -208,6 +213,77 @@ void RemoveDescribedDiscImages(std::vector *files) { }), files->end()); } +bool FindPS1DiscFiles(const std::vector &entries, const std::function &readText, std::vector *discFiles) { + std::map byLowerName; + for (const ArchiveEntry &entry : entries) { + byLowerName[AsciiToLower(ReplaceAll(entry.name, "\\", "/"))] = entry.name; + } + auto find = [&](const std::string &name, std::string *found) { + auto it = byLowerName.find(AsciiToLower(ReplaceAll(name, "\\", "/"))); + if (it == byLowerName.end()) { + return false; + } + *found = it->second; + return true; + }; + auto extension = [](const std::string &name) { + const size_t dot = name.find_last_of("./"); + return dot != std::string::npos && name[dot] == '.' ? AsciiToLower(name.substr(dot)) : std::string(); + }; + + for (const ArchiveEntry &entry : entries) { + const std::string ext = extension(entry.name); + const std::string stem = entry.name.substr(0, entry.name.size() - ext.size()); + const size_t slash = entry.name.find_last_of("/\\"); + const std::string dir = slash == std::string::npos ? "" : entry.name.substr(0, slash + 1); + std::vector files = { entry.name }; + std::string found; + if (ext == ".cue") { + std::string text; + if (!readText(entry.name, &text)) { + continue; + } + const std::vector names = CueSheetFileNamesFromText(text); + bool all = !names.empty(); + for (const std::string &name : names) { + if (find(dir + name, &found) || (names.size() == 1 && (find(stem + ".bin", &found) || find(stem + ".img", &found)))) { + files.push_back(found); + } else { + all = false; + } + } + if (!all) { + continue; + } + } else if (ext == ".ccd") { + if (!find(stem + ".img", &found)) { + continue; + } + files.push_back(found); + if (find(stem + ".sub", &found)) { + files.push_back(found); + } + } else { + continue; + } + *discFiles = files; + return true; + } + + const ArchiveEntry *largest = nullptr; + for (const ArchiveEntry &entry : entries) { + const std::string ext = extension(entry.name); + if ((ext == ".bin" || ext == ".img") && entry.size > 0 && entry.size % SECTOR_SIZE == 0 && (!largest || entry.size > largest->size)) { + largest = &entry; + } + } + if (largest) { + *discFiles = { largest->name }; + return true; + } + return false; +} + PS1EbootFileLoader::PS1EbootFileLoader(const Path &path) : path_(path) { const std::string ext = path.GetFileExtension(); std::string text; diff --git a/Core/FileLoaders/PS1EbootFileLoader.h b/Core/FileLoaders/PS1EbootFileLoader.h index fc8ff6b614..94bbc0286f 100644 --- a/Core/FileLoaders/PS1EbootFileLoader.h +++ b/Core/FileLoaders/PS1EbootFileLoader.h @@ -17,6 +17,7 @@ #pragma once +#include #include #include #include @@ -100,6 +101,18 @@ bool IsPS1CueOrCcd(FileLoader *fileLoader); // The file names a cue sheet's FILE lines give. std::vector CueSheetFileNames(const Path &cuePath); +std::vector CueSheetFileNamesFromText(std::string_view text); + +// A file in an archive: its path there, and its size. +struct ArchiveEntry { + std::string name; + s64 size; +}; + +// The files of a PS1 disc among an archive's, by their paths in it: a cue sheet and the files it names, +// a .ccd with its .img (and .sub), or else the largest .bin or .img that's a whole number of sectors. +// readText reads a small file from the archive. +bool FindPS1DiscFiles(const std::vector &entries, const std::function &readText, std::vector *discFiles); // The cue sheet next to a disc image whose FILE lines name it, or an empty path. Path FindCueSheetFor(const Path &imagePath); diff --git a/Core/Loaders.cpp b/Core/Loaders.cpp index 66f873ec2c..e468bf9bf5 100644 --- a/Core/Loaders.cpp +++ b/Core/Loaders.cpp @@ -599,6 +599,37 @@ bool DetectArchiveContents(VFSInterface *vfs, ZipFileInfo *info) { return true; } } + + // A PS1 disc in the root or one level down, as in a zip. + std::vector entries; + for (const File::FileInfo &file : listing) { + if (!file.isDirectory) { + entries.push_back(ArchiveEntry{ file.fullName.ToString(), (s64)file.size }); + continue; + } + std::vector 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; } @@ -633,6 +664,7 @@ void DetectZipFileContents(zip_t *z, ZipFileInfo *info) { bool hasPRX = false; bool hasPluginIni = false; s64 totalFileSize = 0; + std::vector 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. @@ -659,6 +691,9 @@ void DetectZipFileContents(zip_t *z, ZipFileInfo *info) { 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; @@ -754,11 +789,23 @@ void DetectZipFileContents(zip_t *z, ZipFileInfo *info) { info->ignoreMetaFiles = false; info->totalFileSize = totalFileSize; + // A PS1 disc: a cue sheet or .ccd 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"; + // 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; @@ -780,6 +827,9 @@ void DetectZipFileContents(zip_t *z, ZipFileInfo *info) { 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; } diff --git a/Core/Loaders.h b/Core/Loaders.h index c4311629d0..223703a603 100644 --- a/Core/Loaders.h +++ b/Core/Loaders.h @@ -19,6 +19,7 @@ #include #include +#include #include "Common/CommonTypes.h" #include "Common/File/Path.h" @@ -192,6 +193,8 @@ enum class ZipFileContents { EXTRACTED_GAME, SAVE_STATES, PRX_PLUGIN, + // A PS1 disc image, which can be one file or several (a cue sheet and what it names). + PSX_DISC, }; // TODO: Rename to ArchiveContentsInfo or something. @@ -211,6 +214,7 @@ struct ZipFileInfo { std::string mTime; std::string iniContents; std::string isoFilename; // Used by SevenZ only right now. + std::vector discFiles; // For PSX_DISC, the disc's files in the archive. s64 totalFileSize = 0; std::string contentName; diff --git a/Core/Util/GameManager.cpp b/Core/Util/GameManager.cpp index 9a9b956866..56938ecb7d 100644 --- a/Core/Util/GameManager.cpp +++ b/Core/Util/GameManager.cpp @@ -234,6 +234,19 @@ static std::string ZipReadFileByIndex(struct zip *z, int file_index) { return buffer; } +// Where a PS1 disc's files go: a folder of their own, named after the cue sheet or .ccd, when there's +// more than one. +static Path PS1DiscInstallDir(const Path &destDir, const std::vector &files) { + if (files.size() <= 1) { + return destDir; + } + const Path first(files[0]); + const std::string name = first.GetFilename(); + const Path dir = destDir / name.substr(0, name.size() - first.GetFileExtension().size()); + File::CreateFullPath(dir); + return dir; +} + // Parameters need to be by value, since this is a thread func. void GameManager::InstallZipContents(ZipFileTask task) { SetCurrentThreadName("InstallZipContents"); @@ -288,68 +301,81 @@ void GameManager::InstallZipContents(ZipFileTask task) { if (task.zipFileInfo && task.zipFileInfo->archiveType == ArchiveType::SevenZ) { Path destPath = task.destination; g_OSD.SetProgressBar("install", di->T("Installing..."), 0.0f, 0.0f, 0.0f, 0.1f); - Path fn(task.zipFileInfo->isoFilename); - Path destFile = destPath / fn.GetFilename(); + // An ISO, or a PS1 disc's files. + std::vector names; + if (task.zipFileInfo->contents == ZipFileContents::PSX_DISC) { + names = task.zipFileInfo->discFiles; + destPath = PS1DiscInstallDir(destPath, names); + } else { + names.push_back(task.zipFileInfo->isoFilename); + } const size_t blockSize = 1024 * 128; SevenZipFileReader *reader = SevenZipFileReader::Create(task.fileName, "", true); - VFSFileReference *vfsRef = nullptr; - VFSOpenFile *vfsFile = nullptr; - FILE *f = nullptr; - bool success = false; + bool success = reader != nullptr; bool writeFailed = false; bool readFailed = false; - size_t totalSize = 0; - size_t bytesCopied = 0; + std::vector destFiles; + for (size_t n = 0; n < names.size() && success; n++) { + const std::string &name = names[n]; + const Path destFile = destPath / Path(name).GetFilename(); + VFSFileReference *vfsRef = reader->GetFile(name); + VFSOpenFile *vfsFile = nullptr; + FILE *f = nullptr; + size_t totalSize = 0; + size_t bytesCopied = 0; + success = false; - if (reader) { - vfsRef = reader->GetFile(task.zipFileInfo->isoFilename); - } - if (vfsRef) { - vfsFile = reader->OpenFileForRead(vfsRef, &totalSize); - } - if (vfsFile) { - f = File::OpenCFile(destFile, "wb"); - } - - if (f) { - std::vector buffer(blockSize); - while (bytesCopied < totalSize) { - size_t readSize = std::min(blockSize, totalSize - bytesCopied); - size_t bytesRead = reader->Read(vfsFile, buffer.data(), readSize); - if (bytesRead == 0) { - ERROR_LOG(Log::HLE, "Failed to stream extract 7z ISO '%s'", task.zipFileInfo->isoFilename.c_str()); - readFailed = true; - break; - } - - size_t written = fwrite(buffer.data(), 1, bytesRead, f); - if (written != bytesRead) { - ERROR_LOG(Log::HLE, "Wrote %d bytes out of %d - Disk full?", (int)written, (int)bytesRead); - writeFailed = true; - break; - } - - bytesCopied += bytesRead; - installProgress_ = totalSize > 0 ? (float)bytesCopied / (float)totalSize : 1.0f; - g_OSD.SetProgressBar("install", di->T("Installing..."), 0.0f, 1.0f, installProgress_, 0.1f); + if (vfsRef) { + vfsFile = reader->OpenFileForRead(vfsRef, &totalSize); + } + if (vfsFile) { + f = File::OpenCFile(destFile, "wb"); } - success = bytesCopied == totalSize; - fclose(f); - f = nullptr; - } + if (f) { + destFiles.push_back(destFile); + std::vector buffer(blockSize); + while (bytesCopied < totalSize) { + size_t readSize = std::min(blockSize, totalSize - bytesCopied); + size_t bytesRead = reader->Read(vfsFile, buffer.data(), readSize); + if (bytesRead == 0) { + ERROR_LOG(Log::HLE, "Failed to stream extract 7z file '%s'", name.c_str()); + readFailed = true; + break; + } - if (vfsFile) { - reader->CloseFile(vfsFile); - } - if (vfsRef) { - reader->ReleaseFile(vfsRef); + size_t written = fwrite(buffer.data(), 1, bytesRead, f); + if (written != bytesRead) { + ERROR_LOG(Log::HLE, "Wrote %d bytes out of %d - Disk full?", (int)written, (int)bytesRead); + writeFailed = true; + break; + } + + bytesCopied += bytesRead; + const float fileProgress = totalSize > 0 ? (float)bytesCopied / (float)totalSize : 1.0f; + installProgress_ = ((float)n + fileProgress) / (float)names.size(); + g_OSD.SetProgressBar("install", di->T("Installing..."), 0.0f, 1.0f, installProgress_, 0.1f); + } + + success = bytesCopied == totalSize; + fclose(f); + f = nullptr; + } + + if (vfsFile) { + reader->CloseFile(vfsFile); + } + if (vfsRef) { + reader->ReleaseFile(vfsRef); + } } delete reader; if (!success) { - File::Delete(destFile); + for (const Path &destFile : destFiles) { + File::Delete(destFile); + } if (writeFailed) { SetInstallError(sy->T("Storage full")); } else if (readFailed) { @@ -406,6 +432,12 @@ void GameManager::InstallZipContents(ZipFileTask task) { success = InstallZippedISO(z, zipInfo.isoFileIndex, task.destination); break; } + case ZipFileContents::PSX_DISC: + { + INFO_LOG(Log::HLE, "Installing PS1 disc '%s' into '%s'", task.fileName.c_str(), task.destination.c_str()); + success = InstallZippedPS1Disc(z, zipInfo.discFiles, task.destination); + break; + } case ZipFileContents::TEXTURE_PACK: { Path dest; @@ -921,6 +953,46 @@ bool GameManager::InstallZippedISO(struct zip *z, int isoFileIndex, const Path & return true; } +bool GameManager::InstallZippedPS1Disc(struct zip *z, const std::vector &files, const Path &destDir) { + Path dir = destDir.empty() ? Path(g_Config.currentDirectory) : destDir; + dir = PS1DiscInstallDir(dir, files); + + int64_t allBytes = 1; + std::vector indices; + for (const std::string &file : files) { + const zip_int64_t index = zip_name_locate(z, file.c_str(), 0); + zip_stat_t zstat; + zip_stat_init(&zstat); + if (index < 0 || zip_stat_index(z, index, 0, &zstat) < 0) { + auto iz = GetI18NCategory(I18NCat::INSTALLZIP); + SetInstallError(iz->T("Zip archive corrupt")); + return false; + } + allBytes += zstat.size; + indices.push_back(index); + } + + auto di = GetI18NCategory(I18NCat::DIALOG); + g_OSD.SetProgressBar("install", di->T("Installing..."), 0.0f, 0.0f, 0.0f, 0.1f); + int64_t bytesCopied = 0; + bool success = true; + std::vector written; + for (size_t i = 0; i < files.size() && success; i++) { + const Path outFile = dir / Path(files[i]).GetFilename(); + success = ExtractFile(z, (int)indices[i], outFile, &bytesCopied, allBytes); + written.push_back(outFile); + } + if (!success) { + for (const Path &file : written) { + File::Delete(file); + } + } else { + INFO_LOG(Log::IO, "Unzipped PS1 disc to '%s'", dir.c_str()); + } + // The caller finishes the install. + return success; +} + bool GameManager::InstallZipOnThread(ZipFileTask task) { if (InstallInProgress() || installDonePending_) { return false; diff --git a/Core/Util/GameManager.h b/Core/Util/GameManager.h index 556e5c30eb..3d3df87837 100644 --- a/Core/Util/GameManager.h +++ b/Core/Util/GameManager.h @@ -95,6 +95,9 @@ public: // Extracts the contents of an open zip archive into dest. Exposed for testing. // maxTotalSize limits the total decompressed size (zip bomb protection). bool ExtractZipContents(struct zip *z, const Path &dest, const ZipFileInfo &info, bool allowRoot, int64_t maxTotalSize = 0x100000000ULL); + // Extracts a PS1 disc's files (ZipFileInfo::discFiles), into a folder of their own if there's more + // than one. Exposed for testing. + bool InstallZippedPS1Disc(struct zip *z, const std::vector &files, const Path &destDir); private: void InstallZipContents(ZipFileTask task); diff --git a/Core/Util/PSARUnpack.cpp b/Core/Util/PSARUnpack.cpp index 91962f0dde..d62e5a34b1 100644 --- a/Core/Util/PSARUnpack.cpp +++ b/Core/Util/PSARUnpack.cpp @@ -1365,6 +1365,15 @@ bool FirmwareVersionPlaysUmdVideo(std::string_view version) { return FirmwareVersionSupportsVSH(version) && FirmwareVersionToInt(version) >= FirmwareVersionToInt(UMD_VIDEO_MIN_FIRMWARE); } +bool InstalledFirmwareRunsPS1Discs() { + // The POPS managers PopsBoot knows for converted EBOOTs: 3.95 to 6.61. A version in between that it + // doesn't know fails the boot with a message saying so. + InstalledFirmwareInfo info; + ReadInstalledFirmwareInfo(g_Config.nandRootDirectory, &info, false); + const int version = FirmwareVersionToInt(info.version); + return version >= 395 && version <= 661 && File::Exists(g_Config.nandRootDirectory / "flash0/kd/popsman.prx"); +} + Path GetUmdVideoVSHPath() { InstalledFirmwareInfo info; ReadInstalledFirmwareInfo(g_Config.nandRootDirectory, &info, false); diff --git a/Core/Util/PSARUnpack.h b/Core/Util/PSARUnpack.h index 6662ba489e..5ad9390644 100644 --- a/Core/Util/PSARUnpack.h +++ b/Core/Util/PSARUnpack.h @@ -192,6 +192,9 @@ extern const char *const UMD_VIDEO_MIN_FIRMWARE; // vshmain.prx, if the installed firmware's XMB can play UMD Video discs. Empty otherwise. Path GetUmdVideoVSHPath(); +// Whether the installed firmware's POPS can run a PS1 disc image, as the converted EBOOT made from it. +bool InstalledFirmwareRunsPS1Discs(); + // Installs a firmware into the NAND directory. Two firmwares can't be merged - a file the new one // doesn't have would linger and still get loaded - so this replaces rather than overlays. // diff --git a/UI/GameScreen.cpp b/UI/GameScreen.cpp index cd9fe5cdbc..0d03707e99 100644 --- a/UI/GameScreen.cpp +++ b/UI/GameScreen.cpp @@ -243,6 +243,9 @@ static bool FileTypeIsPlayable(IdentifiedFileType fileType) { case IdentifiedFileType::PSP_UMD_VIDEO_ISO: // Reverse logic. return false; + case IdentifiedFileType::PSX_ISO: + // Through the firmware's POPS. + return InstalledFirmwareRunsPS1Discs(); default: return true; } diff --git a/UI/InstallZipScreen.cpp b/UI/InstallZipScreen.cpp index 66c859edc6..4cec563d72 100644 --- a/UI/InstallZipScreen.cpp +++ b/UI/InstallZipScreen.cpp @@ -84,6 +84,7 @@ void InstallZipScreen::CreateSettingsViews(UI::ViewGroup *parent) { switch (zipFileInfo_.contents) { case ZipFileContents::ISO_FILE: case ZipFileContents::PSP_GAME_DIR: + case ZipFileContents::PSX_DISC: installChoice_ = parent->Add(new Choice(iz->T("Install"), ImageID("I_FOLDER_UPLOAD"))); installChoice_->OnClick.Handle(this, &InstallZipScreen::OnInstall); @@ -146,6 +147,7 @@ void InstallZipScreen::CreateContentViews(UI::ViewGroup *parent) { switch (zipFileInfo_.contents) { case ZipFileContents::ISO_FILE: case ZipFileContents::PSP_GAME_DIR: + case ZipFileContents::PSX_DISC: { std::string_view question = iz->T("Install game from ZIP file?"); @@ -155,7 +157,7 @@ void InstallZipScreen::CreateContentViews(UI::ViewGroup *parent) { leftColumn->Add(new TextView(zipFileInfo_.contentName)); } - if (zipFileInfo_.contents == ZipFileContents::ISO_FILE) { + if (zipFileInfo_.contents == ZipFileContents::ISO_FILE || zipFileInfo_.contents == ZipFileContents::PSX_DISC) { const bool isInDownloads = File::IsProbablyInDownloadsFolder(zipPath_); Path parent; if (!isInDownloads && zipPath_.CanNavigateUp()) { diff --git a/unittest/TestPS1Eboot.cpp b/unittest/TestPS1Eboot.cpp index 6aafd86d6c..394131605b 100644 --- a/unittest/TestPS1Eboot.cpp +++ b/unittest/TestPS1Eboot.cpp @@ -22,6 +22,8 @@ #include #include +#include "ext/libzip/zip.h" + #include "Common/File/DirListing.h" #include "Common/File/FileUtil.h" #include "Common/File/Path.h" @@ -29,6 +31,7 @@ #include "Core/ELF/ParamSFO.h" #include "Core/FileLoaders/PS1EbootFileLoader.h" #include "Core/Loaders.h" +#include "Core/Util/GameManager.h" #include "UnitTest.h" @@ -291,6 +294,46 @@ bool TestPS1Eboot() { const std::vector expectedNames = { "Clone.ccd", "Cooked.iso", "Game.cue", "Loose.bin", "Pregap.cue" }; EXPECT_TRUE(names == expectedNames); + // Installing from a zip: the cue sheet and both its files, into a folder of their own. + const Path zipPath = dir / "Game.zip"; + { + int errorp = 0; + zip_t *z = zip_open(zipPath.c_str(), ZIP_CREATE | ZIP_TRUNCATE, &errorp); + EXPECT_TRUE(z != nullptr); + const std::pair *> entries[] = { + { "Game/Game (Track 1).bin", &data }, + { "Game/Game (Track 2).bin", &audio }, + }; + for (const auto &entry : entries) { + zip_source_t *source = zip_source_buffer(z, entry.second->data(), entry.second->size(), 0); + EXPECT_TRUE(zip_file_add(z, entry.first, source, ZIP_FL_ENC_UTF_8) >= 0); + } + zip_source_t *cueSource = zip_source_buffer(z, cue.data(), cue.size(), 0); + EXPECT_TRUE(zip_file_add(z, "Game/Game.cue", cueSource, ZIP_FL_ENC_UTF_8) >= 0); + zip_source_t *readme = zip_source_buffer(z, "hi", 2, 0); + EXPECT_TRUE(zip_file_add(z, "readme.txt", readme, ZIP_FL_ENC_UTF_8) >= 0); + EXPECT_TRUE(zip_close(z) == 0); + } + { + int errorp = 0; + zip_t *z = zip_open(zipPath.c_str(), 0, &errorp); + EXPECT_TRUE(z != nullptr); + ZipFileInfo info{}; + DetectZipFileContents(z, &info); + EXPECT_TRUE(info.contents == ZipFileContents::PSX_DISC); + EXPECT_EQ_INT(info.discFiles.size(), 3); + EXPECT_EQ_STR(info.discFiles[0], std::string("Game/Game.cue")); + const Path installDir = dir / "installed"; + EXPECT_TRUE(File::CreateFullPath(installDir)); + GameManager manager; + EXPECT_TRUE(manager.InstallZippedPS1Disc(z, info.discFiles, installDir)); + zip_close(z); + PS1EbootFileLoader eboot(installDir / "Game" / "Game.cue"); + if (!CheckEboot(eboot, disc, 2)) { + return false; + } + } + File::DeleteDirRecursively(dir); return true; }