diff --git a/CMakeLists.txt b/CMakeLists.txt index 824a08df54..575b429f7e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2378,6 +2378,8 @@ add_library(${CoreLibName} ${CoreLinkType} Core/FileLoaders/RamCachingFileLoader.h Core/FileLoaders/RetryingFileLoader.cpp Core/FileLoaders/RetryingFileLoader.h + Core/FileLoaders/ZipFileLoader.cpp + Core/FileLoaders/ZipFileLoader.h Core/MIPS/MIPS.cpp Core/MIPS/MIPS.h Core/MIPS/MIPSAnalyst.cpp diff --git a/Common/StringUtils.cpp b/Common/StringUtils.cpp index b179d363b2..d04311906f 100644 --- a/Common/StringUtils.cpp +++ b/Common/StringUtils.cpp @@ -266,6 +266,15 @@ std::string_view KeepAfterLast(std::string_view s, char c) { } } +std::string_view KeepIncludingLast(std::string_view s, char c) { + size_t pos = s.rfind(c); + if (pos != std::string_view::npos) { + return s.substr(pos); + } else { + return s; + } +} + void SkipSpace(const char **ptr) { while (**ptr && isspace(**ptr)) { (*ptr)++; diff --git a/Common/StringUtils.h b/Common/StringUtils.h index 1bfccb2521..28f0d05ba2 100644 --- a/Common/StringUtils.h +++ b/Common/StringUtils.h @@ -99,6 +99,7 @@ std::string StripSpaces(const std::string &s); std::string StripQuotes(const std::string &s); std::string_view KeepAfterLast(std::string_view s, char c); +std::string_view KeepIncludingLast(std::string_view s, char c); std::string_view StripSpaces(std::string_view s); std::string_view StripQuotes(std::string_view s); diff --git a/Core/Core.vcxproj b/Core/Core.vcxproj index 39b6694b3b..6322bcf448 100644 --- a/Core/Core.vcxproj +++ b/Core/Core.vcxproj @@ -565,6 +565,7 @@ + @@ -1186,6 +1187,7 @@ + diff --git a/Core/Core.vcxproj.filters b/Core/Core.vcxproj.filters index ddf9bac3ec..3c50aa21c9 100644 --- a/Core/Core.vcxproj.filters +++ b/Core/Core.vcxproj.filters @@ -1351,6 +1351,9 @@ HLE\Libraries + + FileLoaders + @@ -2184,6 +2187,9 @@ HLE\Libraries + + FileLoaders + diff --git a/Core/FileLoaders/LocalFileLoader.h b/Core/FileLoaders/LocalFileLoader.h index d5b29cbe42..7e1546a995 100644 --- a/Core/FileLoaders/LocalFileLoader.h +++ b/Core/FileLoaders/LocalFileLoader.h @@ -21,6 +21,7 @@ #include "Common/CommonTypes.h" #include "Common/File/Path.h" +#include "Common/StringUtils.h" #include "Core/Loaders.h" #ifdef _WIN32 diff --git a/Core/FileLoaders/ZipFileLoader.cpp b/Core/FileLoaders/ZipFileLoader.cpp new file mode 100644 index 0000000000..5a5c1d6fc3 --- /dev/null +++ b/Core/FileLoaders/ZipFileLoader.cpp @@ -0,0 +1,154 @@ +#pragma once + +#include + +#include "Core/FileLoaders/LocalFileLoader.h" +#include "Core/FileLoaders/ZipFileLoader.h" + +ZipFileLoader::ZipFileLoader(FileLoader *sourceLoader) + : ProxiedFileLoader(sourceLoader), zipArchive_(nullptr) { + if (!backend_ || !backend_->Exists() || backend_->IsDirectory()) { + // bad + } + + zip_error_t error{}; + zip_source_t* zipSource = zip_source_function_create([](void* userdata, void* data, zip_uint64_t len, zip_source_cmd_t cmd) -> zip_int64_t { + ZipFileLoader *loader = (ZipFileLoader *)userdata; + return loader->ZipSourceCallback(data, len, cmd); + }, this, &error); + if (!zipSource) { + ERROR_LOG(Log::IO, "Failed to create ZIP source: %s", zip_error_strerror(&error)); + return; + } + + zipArchive_ = zip_open_from_source(zipSource, ZIP_RDONLY, &error); + if (!zipArchive_) { + ERROR_LOG(Log::IO, "Failed to open ZIP archive: %s", zip_error_strerror(&error)); + zip_source_free(zipSource); + } +} + +ZipFileLoader::~ZipFileLoader() { + if (dataFile_) { + zip_fclose(dataFile_); + } + if (zipArchive_) { + zip_discard(zipArchive_); + } + if (data_) { + free(data_); + } +} + +bool ZipFileLoader::Initialize(int fileIndex) { + _dbg_assert_(!data_); + + struct zip_stat zstat; + int retval = zip_stat_index(zipArchive_, fileIndex, ZIP_FL_NOCASE | ZIP_FL_UNCHANGED, &zstat); + if (retval < 0) { + return false; + } + const char *name = zip_get_name(zipArchive_, fileIndex, ZIP_FL_NOCASE | ZIP_FL_UNCHANGED); + fileExtension_ = KeepIncludingLast(name, '.'); + + _dbg_assert_(zstat.index == fileIndex); + dataFileSize_ = zstat.size; + dataFile_ = zip_fopen_index(zipArchive_, zstat.index, ZIP_FL_UNCHANGED); + data_ = (u8 *)malloc(dataFileSize_); + return data_ != nullptr; +} + +size_t ZipFileLoader::ReadAt(s64 absolutePos, size_t bytes, void *data, Flags flags) { + if (!dataFile_ || absolutePos < 0 || absolutePos >= dataFileSize_) { + return 0; + } + + if (absolutePos + bytes > dataFileSize_) { + // TODO: This could go negative.. + bytes = dataFileSize_ - absolutePos; + } + + // Decompress until the requested point, filling up data_ as we go. TODO: Do on thread. + while (dataReadPos_ < absolutePos + bytes) { + int remaining = BLOCK_SIZE; + if (dataReadPos_ + remaining > dataFileSize_) { + remaining = dataFileSize_ - dataReadPos_; + } + zip_int64_t retval = zip_fread(dataFile_, data_ + dataReadPos_, remaining); + _dbg_assert_(retval == remaining); + dataReadPos_ += retval; + } + + // Perform the read. + memcpy(data, data_ + absolutePos, bytes); + return bytes; +} + +zip_int64_t ZipFileLoader::ZipSourceCallback(void *data, zip_uint64_t len, zip_source_cmd_t cmd) { + switch (cmd) { + case ZIP_SOURCE_OPEN: + { + zipReadPos_ = 0; + return 0; + } + case ZIP_SOURCE_READ: + { + size_t readBytes = static_cast(backend_->ReadAt(zipReadPos_, len, data)); + zipReadPos_ += readBytes; + return readBytes; + } + case ZIP_SOURCE_SEEK: + { + struct SeekData { + zip_int64_t offset; + int whence; + }; + if (len < sizeof(SeekData)) { + return -1; // Invalid argument size + } + const SeekData *seekData = static_cast(data); + zip_int64_t new_offset; + switch (seekData->whence) { + case SEEK_SET: + new_offset = seekData->offset; + break; + case SEEK_CUR: + new_offset = zipReadPos_ + seekData->offset; + break; + case SEEK_END: + new_offset = backend_->FileSize() + seekData->offset; + break; + default: + return -1; // Invalid 'whence' value + } + if (new_offset < 0 || new_offset > backend_->FileSize()) { + return -1; // Offset out of bounds + } + zipReadPos_ = new_offset; + return 0; + } + case ZIP_SOURCE_TELL: + return zipReadPos_; + case ZIP_SOURCE_CLOSE: + return 0; + case ZIP_SOURCE_STAT: + { + if (len < sizeof(zip_stat_t)) { + return -1; + } + zip_stat_t* st = static_cast(data); + zip_stat_init(st); + st->valid = ZIP_STAT_SIZE; + st->size = static_cast(backend_->FileSize()); + return sizeof(zip_stat_t); + } + case ZIP_SOURCE_ERROR: + return -1; + case ZIP_SOURCE_FREE: + return 0; + case ZIP_SOURCE_SUPPORTS: + return zip_source_make_command_bitmap(ZIP_SOURCE_READ, ZIP_SOURCE_SEEK, ZIP_SOURCE_TELL, ZIP_SOURCE_OPEN, ZIP_SOURCE_CLOSE, ZIP_SOURCE_STAT); + default: + return -1; + } +} diff --git a/Core/FileLoaders/ZipFileLoader.h b/Core/FileLoaders/ZipFileLoader.h new file mode 100644 index 0000000000..708a1cf3ba --- /dev/null +++ b/Core/FileLoaders/ZipFileLoader.h @@ -0,0 +1,65 @@ +#pragma once + +#include + +#include + +#include "Common/CommonTypes.h" +#include "Common/Log.h" +#include "Common/File/Path.h" +#include "Common/StringUtils.h" +#include "Core/Loaders.h" + +// Exposes a single (chosen) file from a zip file as another file loader. +// Useful in a bunch of possible chains. +class ZipFileLoader : public ProxiedFileLoader { +public: + ZipFileLoader(FileLoader *sourceLoader); + ~ZipFileLoader() override; + + zip_t *GetZip() const { + return zipArchive_; + } + + bool Initialize(int fileIndex); + + bool Exists() override { + return dataFile_ != nullptr; + } + + bool IsDirectory() override { + return false; + } + + s64 FileSize() override { + return dataFileSize_; + } + + Path GetPath() const override { + return backend_->GetPath(); + } + + size_t ReadAt(s64 absolutePos, size_t bytes, size_t count, void *data, Flags flags = Flags::NONE) override { + return ReadAt(absolutePos, bytes * count, data, flags) / bytes; + } + size_t ReadAt(s64 absolutePos, size_t bytes, void *data, Flags flags = Flags::NONE) override; + + std::string GetFileExtension() const { + return fileExtension_; + } + +private: + zip_int64_t ZipSourceCallback(void* data, zip_uint64_t len, zip_source_cmd_t cmd); + + enum { + BLOCK_SIZE = 65536, + }; + zip_t *zipArchive_ = nullptr; + s64 zipReadPos_ = 0; + + zip_file_t *dataFile_ = nullptr; + uint8_t *data_ = nullptr; // malloc/free + s64 dataReadPos_ = 0; + s64 dataFileSize_ = 0; + std::string fileExtension_; +}; diff --git a/Core/FileSystems/FileSystem.h b/Core/FileSystems/FileSystem.h index ca120905ed..8c46e6f77d 100644 --- a/Core/FileSystems/FileSystem.h +++ b/Core/FileSystems/FileSystem.h @@ -17,6 +17,10 @@ #pragma once +// TODO: Somehow unify FileSystem, VFS and FileLoaders. Actually, maybe FileSystem and VFS have the most in common +// but file systems should be able to contain FileLoader as files. Then we can do stuff like playing homebrew directly +// out of zip files, and similar tricks. + #include #include #include diff --git a/Core/Loaders.cpp b/Core/Loaders.cpp index 157a0f2d1c..59c67236e9 100644 --- a/Core/Loaders.cpp +++ b/Core/Loaders.cpp @@ -18,11 +18,13 @@ #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/RetryingFileLoader.h" +#include "Core/FileLoaders/ZipFileLoader.h" #include "Core/FileSystems/MetaFileSystem.h" #include "Core/PSPLoaders.h" #include "Core/MemMap.h" @@ -31,6 +33,7 @@ #include "Core/System.h" #include "Core/ELF/PBPReader.h" #include "Core/ELF/ParamSFO.h" +#include "Core/Util/GameManager.h" FileLoader *ConstructFileLoader(const Path &filename) { if (filename.Type() == PathType::HTTP) { @@ -62,7 +65,7 @@ IdentifiedFileType Identify_File(FileLoader *fileLoader, std::string *errorStrin return IdentifiedFileType::ERROR_IDENTIFYING; } - std::string extension = fileLoader->GetPath().GetFileExtension(); + std::string extension = fileLoader->GetFileExtension(); if (extension == ".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) { @@ -229,6 +232,29 @@ FileLoader *ResolveFileLoaderTarget(FileLoader *fileLoader) { delete fileLoader; fileLoader = ConstructFileLoader(ebootFilename); } + } else if (type == 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); + return zipLoader; + } + default: + { + // Nothing runnable in file. Take the original loader back and return it. + fileLoader = zipLoader->Steal(); + delete zipLoader; + return fileLoader; + } + } } return fileLoader; } diff --git a/Core/Loaders.h b/Core/Loaders.h index 71f12afae2..b58eda7629 100644 --- a/Core/Loaders.h +++ b/Core/Loaders.h @@ -57,6 +57,7 @@ enum class IdentifiedFileType { }; // NB: It is a REQUIREMENT that implementations of this class are entirely thread safe! +// TOOD: actually, is it really? class FileLoader { public: enum class Flags { @@ -77,7 +78,9 @@ public: virtual bool IsDirectory() = 0; virtual s64 FileSize() = 0; virtual Path GetPath() const = 0; - + virtual std::string GetFileExtension() const { + return GetPath().GetFileExtension(); + } virtual size_t ReadAt(s64 absolutePos, size_t bytes, size_t count, void *data, Flags flags = Flags::NONE) = 0; virtual size_t ReadAt(s64 absolutePos, size_t bytes, void *data, Flags flags = Flags::NONE) { return ReadAt(absolutePos, 1, bytes, data, flags); @@ -98,7 +101,6 @@ public: // Takes ownership. delete backend_; } - bool IsRemote() override { return backend_->IsRemote(); } @@ -129,6 +131,11 @@ public: size_t ReadAt(s64 absolutePos, size_t bytes, void *data, Flags flags = Flags::NONE) override { return backend_->ReadAt(absolutePos, bytes, data, flags); } + FileLoader *Steal() { + FileLoader *backend = backend_; + backend_ = nullptr; + return backend; + } protected: FileLoader *backend_; diff --git a/Core/Util/GameManager.cpp b/Core/Util/GameManager.cpp index cfda50e21a..2b109aaca4 100644 --- a/Core/Util/GameManager.cpp +++ b/Core/Util/GameManager.cpp @@ -256,6 +256,7 @@ bool CanExtractWithoutOverwrite(struct zip *z, const Path &destination, int maxO void DetectZipFileContents(struct zip *z, ZipFileInfo *info) { int numFiles = zip_get_num_files(z); + _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 @@ -263,6 +264,7 @@ void DetectZipFileContents(struct zip *z, ZipFileInfo *info) { bool isPSPMemstickGame = false; bool isZippedISO = false; bool isTexturePack = false; + bool isFrameDump = false; int stripChars = 0; int isoFileIndex = -1; int stripCharsTexturePack = -1; @@ -323,6 +325,10 @@ void DetectZipFileContents(struct zip *z, ZipFileInfo *info) { isTexturePack = true; textureIniIndex = i; } + } else if (endsWith(zippedName, ".ppdmp")) { + isFrameDump = true; + isoFileIndex = i; + info->contentName = zippedName; } else if (endsWith(zippedName, "/param.sfo")) { // Get the game name so we can display it. std::string paramSFOContents; @@ -368,6 +374,8 @@ void DetectZipFileContents(struct zip *z, ZipFileInfo *info) { } else if (stripChars == 0 && filesInRoot == 0 && hasParamSFO && hasIcon0PNG) { // As downloaded from GameFAQs, for example. info->contents = ZipFileContents::SAVE_DATA; + } else if (isFrameDump) { + info->contents = ZipFileContents::FRAME_DUMP; } else { info->contents = ZipFileContents::UNKNOWN; } diff --git a/Core/Util/GameManager.h b/Core/Util/GameManager.h index 27d7422d2b..64807b6b42 100644 --- a/Core/Util/GameManager.h +++ b/Core/Util/GameManager.h @@ -41,6 +41,7 @@ enum class ZipFileContents { ISO_FILE, TEXTURE_PACK, SAVE_DATA, + FRAME_DUMP, }; struct ZipFileInfo { diff --git a/UI/InstallZipScreen.cpp b/UI/InstallZipScreen.cpp index 15ef41c57e..f898cbf651 100644 --- a/UI/InstallZipScreen.cpp +++ b/UI/InstallZipScreen.cpp @@ -31,6 +31,7 @@ #include "UI/MainScreen.h" #include "UI/OnScreenDisplay.h" #include "UI/SavedataScreen.h" +#include "UI/EmuScreen.h" InstallZipScreen::InstallZipScreen(const Path &zipPath) : zipPath_(zipPath) { g_GameManager.ResetInstallError(); @@ -64,8 +65,8 @@ void InstallZipScreen::CreateViews() { bool showDeleteCheckbox = false; returnToHomebrew_ = false; installChoice_ = nullptr; + playChoice_ = nullptr; doneView_ = nullptr; - installChoice_ = nullptr; existingSaveView_ = nullptr; destFolders_.clear(); @@ -101,6 +102,9 @@ void InstallZipScreen::CreateViews() { installChoice_ = rightColumnItems->Add(new Choice(iz->T("Install"))); installChoice_->OnClick.Handle(this, &InstallZipScreen::OnInstall); + playChoice_ = rightColumnItems->Add(new Choice(ga->T("Play"))); + playChoice_->OnClick.Handle(this, &InstallZipScreen::OnPlay); + returnToHomebrew_ = true; showDeleteCheckbox = true; } else if (zipFileInfo_.contents == ZipFileContents::TEXTURE_PACK) { @@ -160,6 +164,11 @@ void InstallZipScreen::CreateViews() { doneView_ = leftColumn->Add(new TextView("")); showDeleteCheckbox = true; + } else if (zipFileInfo_.contents == ZipFileContents::FRAME_DUMP) { + leftColumn->Add(new TextView(zipFileInfo_.contentName)); + // It's a frame dump, add a play button! + playChoice_ = rightColumnItems->Add(new Choice(ga->T("Play"))); + playChoice_->OnClick.Handle(this, &InstallZipScreen::OnPlay); } else { leftColumn->Add(new TextView(iz->T("Zip file does not contain PSP software"), ALIGN_LEFT, false, new AnchorLayoutParams(10, 10, NONE, NONE))); } @@ -212,6 +221,11 @@ UI::EventReturn InstallZipScreen::OnInstall(UI::EventParams ¶ms) { return UI::EVENT_DONE; } +UI::EventReturn InstallZipScreen::OnPlay(UI::EventParams ¶ms) { + screenManager()->switchScreen(new EmuScreen(zipPath_)); + return UI::EVENT_DONE; +} + void InstallZipScreen::update() { auto iz = GetI18NCategory(I18NCat::INSTALLZIP); diff --git a/UI/InstallZipScreen.h b/UI/InstallZipScreen.h index 68982187ac..6ef4adaf84 100644 --- a/UI/InstallZipScreen.h +++ b/UI/InstallZipScreen.h @@ -41,8 +41,10 @@ protected: private: UI::EventReturn OnInstall(UI::EventParams ¶ms); + UI::EventReturn OnPlay(UI::EventParams ¶ms); UI::Choice *installChoice_ = nullptr; + UI::Choice *playChoice_ = nullptr; UI::Choice *backChoice_ = nullptr; UI::TextView *doneView_ = nullptr; SavedataView *existingSaveView_ = nullptr; diff --git a/UWP/CoreUWP/CoreUWP.vcxproj b/UWP/CoreUWP/CoreUWP.vcxproj index 03cfb098a0..7c4bfc9a19 100644 --- a/UWP/CoreUWP/CoreUWP.vcxproj +++ b/UWP/CoreUWP/CoreUWP.vcxproj @@ -165,6 +165,7 @@ + @@ -429,6 +430,7 @@ + @@ -1048,4 +1050,4 @@ - + \ No newline at end of file diff --git a/UWP/CoreUWP/CoreUWP.vcxproj.filters b/UWP/CoreUWP/CoreUWP.vcxproj.filters index 63011547e3..96ecb4279e 100644 --- a/UWP/CoreUWP/CoreUWP.vcxproj.filters +++ b/UWP/CoreUWP/CoreUWP.vcxproj.filters @@ -312,6 +312,9 @@ FileLoaders + + FileLoaders + HLE @@ -1406,6 +1409,9 @@ FileLoaders + + FileLoaders + HLE diff --git a/android/jni/Android.mk b/android/jni/Android.mk index 64c3cbe70e..8e2eb59194 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -611,6 +611,7 @@ EXEC_AND_LIB_FILES := \ $(SRC)/Core/FileLoaders/LocalFileLoader.cpp \ $(SRC)/Core/FileLoaders/RamCachingFileLoader.cpp \ $(SRC)/Core/FileLoaders/RetryingFileLoader.cpp \ + $(SRC)/Core/FileLoaders/ZipFileLoader.cpp \ $(SRC)/Core/MemFault.cpp \ $(SRC)/Core/MemMap.cpp \ $(SRC)/Core/MemMapFunctions.cpp \ diff --git a/libretro/Makefile.common b/libretro/Makefile.common index 35bda56328..148f5ab113 100644 --- a/libretro/Makefile.common +++ b/libretro/Makefile.common @@ -660,6 +660,7 @@ SOURCES_CXX += \ $(COREDIR)/FileLoaders/RetryingFileLoader.cpp \ $(COREDIR)/FileLoaders/RamCachingFileLoader.cpp \ $(COREDIR)/FileLoaders/LocalFileLoader.cpp \ + $(COREDIR)/FileLoaders/ZipFileLoader.cpp \ $(COREDIR)/CoreTiming.cpp \ $(COREDIR)/CwCheat.cpp \ $(COREDIR)/HDRemaster.cpp \