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 \