diff --git a/Core/Loaders.cpp b/Core/Loaders.cpp index 287789bea1..6b13248cc1 100644 --- a/Core/Loaders.cpp +++ b/Core/Loaders.cpp @@ -39,6 +39,7 @@ #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 { @@ -517,7 +518,9 @@ void DetectZipFileContents(zip_t *z, ZipFileInfo *info) { 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; @@ -590,7 +593,7 @@ void DetectZipFileContents(zip_t *z, ZipFileInfo *info) { info->gameTitle = fn; } else if (endsWith(lowerCaseName, "psp_game/sysdir/eboot.bin") || endsWith(lowerCaseName, "psp_game/sysdir/boot.bin")) { isExtractedISO = true; - } else if (endsWith(lowerCaseName, "/param.sfo")) { + } 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)) { @@ -605,6 +608,9 @@ void DetectZipFileContents(zip_t *z, ZipFileInfo *info) { 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; + } } } } @@ -632,6 +638,8 @@ void DetectZipFileContents(zip_t *z, ZipFileInfo *info) { } if (slashCount == 0) { filesInRoot++; + } else { + filesInSubdirs++; } } @@ -652,6 +660,12 @@ void DetectZipFileContents(zip_t *z, ZipFileInfo *info) { } 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) { diff --git a/Core/Loaders.h b/Core/Loaders.h index a9d86dea99..13108bef46 100644 --- a/Core/Loaders.h +++ b/Core/Loaders.h @@ -191,6 +191,7 @@ struct ZipFileInfo { int isoFileIndex = -1; // for ISO int textureIniIndex = -1; // for textures bool ignoreMetaFiles = false; + bool saveDataInRoot = false; // for SAVE_DATA - the files of a single save are in the root, extract into savedataDir. std::string gameTitle; // from PARAM.SFO if available std::string savedataTitle; std::string savedataDetails; diff --git a/Core/Util/GameManager.cpp b/Core/Util/GameManager.cpp index 6d368c3a95..f0b4cc6931 100644 --- a/Core/Util/GameManager.cpp +++ b/Core/Util/GameManager.cpp @@ -414,7 +414,11 @@ void GameManager::InstallZipContents(ZipFileTask task) { case ZipFileContents::SAVE_DATA: { Path pspSaveData = GetSysDirectory(DIRECTORY_SAVEDATA); - success = ExtractZipContents(z, pspSaveData, zipInfo, false); + if (zipInfo.saveDataInRoot) { + success = ExtractZipContents(z, pspSaveData / zipInfo.savedataDir, zipInfo, true); + } else { + success = ExtractZipContents(z, pspSaveData, zipInfo, false); + } break; } case ZipFileContents::SAVE_STATES: diff --git a/UI/InstallZipScreen.cpp b/UI/InstallZipScreen.cpp index 140b4040ed..4a2c94c6c6 100644 --- a/UI/InstallZipScreen.cpp +++ b/UI/InstallZipScreen.cpp @@ -249,7 +249,7 @@ void InstallZipScreen::CreateContentViews(UI::ViewGroup *parent) { Path savedataDir = GetSysDirectory(DIRECTORY_SAVEDATA); ZipContainer zipFile = ZipOpenPath(zipPath_); - overwrite = !ZipCanExtractWithoutOverwrite(zipFile, savedataDir, zipFileInfo_.stripChars, 50); + overwrite = !ZipCanExtractWithoutOverwrite(zipFile, zipFileInfo_.saveDataInRoot ? savedataDir / zipFileInfo_.savedataDir : savedataDir, zipFileInfo_.stripChars, 50); ZipClose(zipFile); destFolders_.push_back(savedataDir); diff --git a/unittest/TestZipSlip.cpp b/unittest/TestZipSlip.cpp index 3f3621a787..623206d091 100644 --- a/unittest/TestZipSlip.cpp +++ b/unittest/TestZipSlip.cpp @@ -2,6 +2,7 @@ #include "Common/File/FileUtil.h" #include "Common/File/Path.h" +#include "Core/ELF/ParamSFO.h" #include "Core/Loaders.h" #include "Core/Util/GameManager.h" #include "Core/Util/PathUtil.h" @@ -123,6 +124,66 @@ static bool TestSaveStateZipExtraction() { return true; } +// A single save folder's contents zipped without the folder (named ULUS10019000.zip, say). The folder +// name comes from SAVEDATA_DIRECTORY in the PARAM.SFO. +static bool TestRootSaveDataZip() { + Path tempRoot = File::GetCurDirectory() / "unittest_zip_savedata_test"; + File::DeleteDirRecursively(tempRoot); + EXPECT_TRUE(File::CreateDir(tempRoot)); + + Path destDir = tempRoot / "SAVEDATA"; + EXPECT_TRUE(File::CreateDir(destDir)); + + std::string sfoContents; + for (const char *saveDir : { "ULUS10019000", "../evil" }) { + ParamSFOData sfo; + sfo.SetValue("CATEGORY", "MS", 4); + sfo.SetValue("TITLE", "CODED ARMS", 128); + sfo.SetValue("SAVEDATA_DIRECTORY", saveDir, 64); + u8 *sfoData = nullptr; + size_t sfoSize = 0; + sfo.WriteSFO(&sfoData, &sfoSize); + sfoContents.assign((const char *)sfoData, sfoSize); + delete[] sfoData; + + Path zipPath = tempRoot / "save.zip"; + int errorp = 0; + zip_t *z = zip_open(zipPath.c_str(), ZIP_CREATE | ZIP_TRUNCATE, &errorp); + EXPECT_TRUE(z != nullptr); + static const char *const contents = "data"; + zip_source_t *source = zip_source_buffer(z, sfoContents.data(), sfoContents.size(), 0); + EXPECT_TRUE(zip_file_add(z, "PARAM.SFO", source, ZIP_FL_ENC_UTF_8) >= 0); + for (const char *name : { "ICON0.PNG", "CODED.ARM" }) { + source = zip_source_buffer(z, contents, strlen(contents), 0); + EXPECT_TRUE(zip_file_add(z, name, source, ZIP_FL_ENC_UTF_8) >= 0); + } + EXPECT_TRUE(zip_close(z) == 0); + + z = zip_open(zipPath.c_str(), 0, &errorp); + EXPECT_TRUE(z != nullptr); + ZipFileInfo info; + DetectZipFileContents(z, &info); + if (!strcmp(saveDir, "../evil")) { + EXPECT_EQ_INT((int)info.contents, (int)ZipFileContents::UNKNOWN); + zip_close(z); + continue; + } + EXPECT_EQ_INT((int)info.contents, (int)ZipFileContents::SAVE_DATA); + EXPECT_TRUE(info.saveDataInRoot); + EXPECT_EQ_STR(info.savedataDir, std::string("ULUS10019000")); + + GameManager manager; + EXPECT_TRUE(manager.ExtractZipContents(z, destDir / info.savedataDir, info, true)); + zip_close(z); + + EXPECT_TRUE(File::Exists(destDir / "ULUS10019000" / "PARAM.SFO")); + EXPECT_TRUE(File::Exists(destDir / "ULUS10019000" / "CODED.ARM")); + } + + File::DeleteDirRecursively(tempRoot); + return true; +} + static bool TestIsSafePathComponent() { EXPECT_TRUE(IsSafePathComponent("ULUS10041")); EXPECT_TRUE(IsSafePathComponent("NPJH-50465")); @@ -151,5 +212,7 @@ bool TestZipSlip() { return false; if (!TestSaveStateZipExtraction()) return false; + if (!TestRootSaveDataZip()) + return false; return true; }