// 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 "Common/Log.h" #include "Common/Data/Text/I18n.h" #include "Common/Data/Format/ZIMLoad.h" #include "Common/Serialize/Serializer.h" #include "Common/Serialize/SerializeFuncs.h" #include "Common/StringUtils.h" #include "Core/Config.h" #include "Core/Host.h" #include "Core/Reporting.h" #include "Core/System.h" #include "Core/Debugger/MemBlockInfo.h" #include "Core/Dialog/SavedataParam.h" #include "Core/Dialog/PSPSaveDialog.h" #include "Core/FileSystems/MetaFileSystem.h" #include "Core/HLE/sceIo.h" #include "Core/HLE/sceKernelMemory.h" #include "Core/HLE/sceChnnlsv.h" #include "Core/ELF/ParamSFO.h" #include "Core/HW/MemoryStick.h" #include "Core/Util/PPGeDraw.h" #include static const std::string ICON0_FILENAME = "ICON0.PNG"; static const std::string ICON1_FILENAME = "ICON1.PMF"; static const std::string PIC1_FILENAME = "PIC1.PNG"; static const std::string SND0_FILENAME = "SND0.AT3"; static const std::string SFO_FILENAME = "PARAM.SFO"; static const std::string savePath = "ms0:/PSP/SAVEDATA/"; namespace { int getSizeNormalized(int size) { int sizeCluster = (int)MemoryStick_SectorSize(); return ((int)((size + sizeCluster - 1) / sizeCluster)) * sizeCluster; } void SetStringFromSFO(ParamSFOData &sfoFile, const char *name, char *str, int strLength) { std::string value = sfoFile.GetValueString(name); truncate_cpy(str, strLength, value.c_str()); } bool ReadPSPFile(std::string filename, u8 **data, s64 dataSize, s64 *readSize) { int handle = pspFileSystem.OpenFile(filename, FILEACCESS_READ); if (handle < 0) return false; if (dataSize == -1) { // Determine the size through seeking instead of querying. pspFileSystem.SeekFile(handle, 0, FILEMOVE_END); dataSize = pspFileSystem.GetSeekPos(handle); pspFileSystem.SeekFile(handle, 0, FILEMOVE_BEGIN); *data = new u8[(size_t)dataSize]; } size_t result = pspFileSystem.ReadFile(handle, *data, dataSize); pspFileSystem.CloseFile(handle); if(readSize) *readSize = result; return result != 0; } bool WritePSPFile(std::string filename, u8 *data, SceSize dataSize) { int handle = pspFileSystem.OpenFile(filename, (FileAccess)(FILEACCESS_WRITE | FILEACCESS_CREATE | FILEACCESS_TRUNCATE)); if (handle < 0) return false; size_t result = pspFileSystem.WriteFile(handle, data, dataSize); pspFileSystem.CloseFile(handle); return result == dataSize; } bool PSPMatch(std::string text, std::string regexp) { if(text.empty() && regexp.empty()) return true; else if(regexp == "*") return true; else if(text.empty()) return false; else if(regexp.empty()) return false; else if(regexp == "?" && text.length() == 1) return true; else if(text == regexp) return true; else if(regexp.data()[0] == '*') { bool res = PSPMatch(text.substr(1),regexp.substr(1)); if(!res) res = PSPMatch(text.substr(1),regexp); return res; } else if(regexp.data()[0] == '?') { return PSPMatch(text.substr(1),regexp.substr(1)); } else if(regexp.data()[0] == text.data()[0]) { return PSPMatch(text.substr(1),regexp.substr(1)); } return false; } int align16(int address) { return ((address + 0xF) >> 4) << 4; } int GetSDKMainVersion(int sdkVersion) { if(sdkVersion > 0x0307FFFF) return 6; if(sdkVersion > 0x0300FFFF) return 5; if(sdkVersion > 0x0206FFFF) return 4; if(sdkVersion > 0x0205FFFF) return 3; if(sdkVersion >= 0x02000000) return 2; if(sdkVersion >= 0x01000000) return 1; return 0; }; } void SaveFileInfo::DoState(PointerWrap &p) { auto s = p.Section("SaveFileInfo", 1, 2); if (!s) return; Do(p, size); Do(p, saveName); Do(p, idx); DoArray(p, title, sizeof(title)); DoArray(p, saveTitle, sizeof(saveTitle)); DoArray(p, saveDetail, sizeof(saveDetail)); Do(p, modif_time); if (s <= 1) { u32 textureData; int textureWidth; int textureHeight; Do(p, textureData); Do(p, textureWidth); Do(p, textureHeight); if (textureData != 0) { // Must be MODE_READ. texture = new PPGeImage(""); texture->CompatLoad(textureData, textureWidth, textureHeight); } } else { bool hasTexture = texture != NULL; Do(p, hasTexture); if (hasTexture) { if (p.mode == p.MODE_READ) { delete texture; texture = new PPGeImage(""); } texture->DoState(p); } } } SavedataParam::SavedataParam() : pspParam(0) , selectedSave(0) , saveDataList(0) , noSaveIcon(0) , saveDataListCount(0) , saveNameListDataCount(0) { } void SavedataParam::Init() { if (!pspFileSystem.GetFileInfo(savePath).exists) { pspFileSystem.MkDir(savePath); } // Create a nomedia file to hide save icons form Android image viewer #if PPSSPP_PLATFORM(ANDROID) int handle = pspFileSystem.OpenFile(savePath + ".nomedia", (FileAccess)(FILEACCESS_CREATE | FILEACCESS_WRITE), 0); if (handle >= 0) { pspFileSystem.CloseFile(handle); } else { INFO_LOG(IO, "Failed to create .nomedia file (might be ok if it already exists)"); } #endif } std::string SavedataParam::GetSaveDirName(const SceUtilitySavedataParam *param, int saveId) const { if (!param) { return ""; } if (saveId >= 0 && saveNameListDataCount > 0) // if user selection, use it return GetFilename(saveId); else return GetSaveName(param); } std::string SavedataParam::GetSaveDir(const SceUtilitySavedataParam *param, const std::string &saveDirName) const { if (!param) { return ""; } return GetGameName(param) + saveDirName; } std::string SavedataParam::GetSaveDir(const SceUtilitySavedataParam *param, int saveId) const { return GetSaveDir(param, GetSaveDirName(param, saveId)); } std::string SavedataParam::GetSaveFilePath(const SceUtilitySavedataParam *param, const std::string &saveDir) const { if (!param) { return ""; } if (!saveDir.size()) return ""; return savePath + saveDir; } std::string SavedataParam::GetSaveFilePath(const SceUtilitySavedataParam *param, int saveId) const { return GetSaveFilePath(param, GetSaveDir(param, saveId)); } inline static std::string FixedToString(const char *str, size_t n) { if (!str) { return std::string(""); } else { return std::string(str, strnlen(str, n)); } } std::string SavedataParam::GetGameName(const SceUtilitySavedataParam *param) const { return FixedToString(param->gameName, ARRAY_SIZE(param->gameName)); } std::string SavedataParam::GetSaveName(const SceUtilitySavedataParam *param) const { const std::string saveName = FixedToString(param->saveName, ARRAY_SIZE(param->saveName)); if (saveName == "<>") return ""; return saveName; } std::string SavedataParam::GetFileName(const SceUtilitySavedataParam *param) const { return FixedToString(param->fileName, ARRAY_SIZE(param->fileName)); } std::string SavedataParam::GetKey(const SceUtilitySavedataParam *param) const { static const char* const lut = "0123456789ABCDEF"; std::string output; if (HasKey(param)) { output.reserve(2 * sizeof(param->key)); for (size_t i = 0; i < sizeof(param->key); ++i) { const unsigned char c = param->key[i]; output.push_back(lut[c >> 4]); output.push_back(lut[c & 15]); } } return output; } bool SavedataParam::HasKey(const SceUtilitySavedataParam *param) const { for (size_t i = 0; i < ARRAY_SIZE(param->key); ++i) { if (param->key[i] != 0) return true; } return false; } bool SavedataParam::Delete(SceUtilitySavedataParam* param, int saveId) { if (!param) { return false; } // Sanity check, preventing full delete of savedata/ in MGS PW demo (!) if (!strlen(param->gameName) && param->mode != SCE_UTILITY_SAVEDATA_TYPE_LISTALLDELETE) { ERROR_LOG(SCEUTILITY, "Bad param with gameName empty - cannot delete save directory"); return false; } std::string dirPath = GetSaveFilePath(param, GetSaveDir(saveId)); if (dirPath.size() == 0) { ERROR_LOG(SCEUTILITY, "GetSaveFilePath returned empty - cannot delete save directory"); return false; } if (!pspFileSystem.GetFileInfo(dirPath).exists) { return false; } pspFileSystem.RmDir(dirPath); return true; } int SavedataParam::DeleteData(SceUtilitySavedataParam* param) { if (!param) { return SCE_UTILITY_SAVEDATA_ERROR_RW_FILE_NOT_FOUND; } std::string subFolder = GetGameName(param) + GetSaveName(param); std::string fileName = GetFileName(param); std::string dirPath = savePath + subFolder; std::string filePath = dirPath + "/" + fileName; std::string sfoPath = dirPath + "/" + SFO_FILENAME; if (!pspFileSystem.GetFileInfo(dirPath).exists) { return SCE_UTILITY_SAVEDATA_ERROR_RW_NO_DATA; } if (!pspFileSystem.GetFileInfo(sfoPath).exists) return SCE_UTILITY_SAVEDATA_ERROR_RW_DATA_BROKEN; if (fileName != "" && !pspFileSystem.GetFileInfo(filePath).exists) { return SCE_UTILITY_SAVEDATA_ERROR_RW_FILE_NOT_FOUND; } if (fileName == "") { return 0; } if (!subFolder.size()) { ERROR_LOG(SCEUTILITY, "Bad subfolder, ignoring delete of %s", filePath.c_str()); return 0; } pspFileSystem.RemoveFile(filePath); return 0; } int SavedataParam::Save(SceUtilitySavedataParam* param, const std::string &saveDirName, bool secureMode) { if (!param) { return SCE_UTILITY_SAVEDATA_ERROR_SAVE_MS_NOSPACE; } if (param->dataSize > param->dataBufSize) { ERROR_LOG_REPORT(SCEUTILITY, "Savedata buffer overflow: %d / %d", param->dataSize, param->dataBufSize); return SCE_UTILITY_SAVEDATA_ERROR_RW_BAD_PARAMS; } auto validateSize = [](const PspUtilitySavedataFileData &data) { if (data.buf.IsValid() && data.bufSize < data.size) { ERROR_LOG_REPORT(SCEUTILITY, "Savedata subdata buffer overflow: %d / %d", data.size, data.bufSize); return false; } return true; }; if (!validateSize(param->icon0FileData) || !validateSize(param->icon1FileData) || !validateSize(param->pic1FileData) || !validateSize(param->snd0FileData)) { return SCE_UTILITY_SAVEDATA_ERROR_RW_BAD_PARAMS; } if (param->secureVersion > 3) { ERROR_LOG_REPORT(SCEUTILITY, "Savedata version requested on save: %d", param->secureVersion); return SCE_UTILITY_SAVEDATA_ERROR_SAVE_PARAM; } else if (param->secureVersion != 0) { if (param->secureVersion != 1 && !HasKey(param)) { ERROR_LOG_REPORT(SCEUTILITY, "Savedata version with missing key on save: %d", param->secureVersion); return SCE_UTILITY_SAVEDATA_ERROR_SAVE_PARAM; } WARN_LOG_REPORT(SCEUTILITY, "Savedata version requested on save: %d", param->secureVersion); } std::string dirPath = GetSaveFilePath(param, GetSaveDir(param, saveDirName)); if (!pspFileSystem.GetFileInfo(dirPath).exists) { if (!pspFileSystem.MkDir(dirPath)) { auto err = GetI18NCategory("Error"); host->NotifyUserMessage(err->T("Unable to write savedata, disk may be full")); } } u8* cryptedData = 0; int cryptedSize = 0; u8 cryptedHash[0x10]{}; // Encrypt save. // TODO: Is this the correct difference between MAKEDATA and MAKEDATASECURE? if (param->dataBuf.IsValid() && g_Config.bEncryptSave && secureMode) { cryptedSize = param->dataSize; if(cryptedSize == 0 || (SceSize)cryptedSize > param->dataBufSize) cryptedSize = param->dataBufSize; // fallback, should never use this u8 *data_ = param->dataBuf; int aligned_len = align16(cryptedSize); cryptedData = new u8[aligned_len + 0x10]; memcpy(cryptedData, data_, cryptedSize); int decryptMode = DetermineCryptMode(param); bool hasKey = decryptMode > 1; if (hasKey && !HasKey(param)) { delete[] cryptedData; return SCE_UTILITY_SAVEDATA_ERROR_SAVE_PARAM; } if (EncryptData(decryptMode, cryptedData, &cryptedSize, &aligned_len, cryptedHash, (hasKey ? param->key : 0)) != 0) { auto err = GetI18NCategory("Error"); host->NotifyUserMessage(err->T("Save encryption failed. This save won't work on real PSP"), 6.0f); ERROR_LOG(SCEUTILITY,"Save encryption failed. This save won't work on real PSP"); delete[] cryptedData; cryptedData = 0; } } // SAVE PARAM.SFO ParamSFOData sfoFile; std::string sfopath = dirPath+"/" + SFO_FILENAME; PSPFileInfo sfoInfo = pspFileSystem.GetFileInfo(sfopath); if (sfoInfo.exists) // Read old sfo if exist { std::vector sfoData; if (pspFileSystem.ReadEntireFile(sfopath, sfoData) >= 0) sfoFile.ReadSFO(sfoData); } // Update values sfoFile.SetValue("TITLE",param->sfoParam.title,128); sfoFile.SetValue("SAVEDATA_TITLE",param->sfoParam.savedataTitle,128); sfoFile.SetValue("SAVEDATA_DETAIL",param->sfoParam.detail,1024); sfoFile.SetValue("PARENTAL_LEVEL",param->sfoParam.parentalLevel,4); sfoFile.SetValue("CATEGORY","MS",4); sfoFile.SetValue("SAVEDATA_DIRECTORY", GetSaveDir(param, saveDirName), 64); // Always write and update the file list. // For each file, 13 bytes for filename, 16 bytes for file hash (0 in PPSSPP), 3 byte for padding const int FILE_LIST_COUNT_MAX = 99; const u32 FILE_LIST_TOTAL_SIZE = sizeof(SaveSFOFileListEntry) * FILE_LIST_COUNT_MAX; u32 tmpDataSize = 0; SaveSFOFileListEntry *tmpDataOrig = (SaveSFOFileListEntry *)sfoFile.GetValueData("SAVEDATA_FILE_LIST", &tmpDataSize); SaveSFOFileListEntry *updatedList = new SaveSFOFileListEntry[FILE_LIST_COUNT_MAX]; if (tmpDataSize != 0) memcpy(updatedList, tmpDataOrig, std::min(tmpDataSize, FILE_LIST_TOTAL_SIZE)); if (tmpDataSize < FILE_LIST_TOTAL_SIZE) memset(updatedList + tmpDataSize, 0, FILE_LIST_TOTAL_SIZE - tmpDataSize); // Leave a hash there and unchanged if it was already there. if (secureMode && param->dataBuf.IsValid()) { const std::string saveFilename = GetFileName(param); for (auto entry = updatedList; entry < updatedList + FILE_LIST_COUNT_MAX; ++entry) { if (entry->filename[0] != '\0') { if (strncmp(entry->filename, saveFilename.c_str(), sizeof(entry->filename)) != 0) continue; } snprintf(entry->filename, sizeof(entry->filename), "%s", saveFilename.c_str()); memcpy(entry->hash, cryptedHash, 16); break; } } sfoFile.SetValue("SAVEDATA_FILE_LIST", (u8 *)updatedList, FILE_LIST_TOTAL_SIZE, (int)FILE_LIST_TOTAL_SIZE); delete[] updatedList; // Init param with 0. This will be used to detect crypted save or not on loading u8 *tmpData = new u8[128]; memset(tmpData, 0, 128); sfoFile.SetValue("SAVEDATA_PARAMS", tmpData, 128, 128); delete[] tmpData; u8 *sfoData; size_t sfoSize; sfoFile.WriteSFO(&sfoData,&sfoSize); // Calc SFO hash for PSP. if(cryptedData != 0) { int offset = sfoFile.GetDataOffset(sfoData,"SAVEDATA_PARAMS"); if(offset >= 0) UpdateHash(sfoData, (int)sfoSize, offset, DetermineCryptMode(param)); } WritePSPFile(sfopath, sfoData, (SceSize)sfoSize); delete[] sfoData; if(param->dataBuf.IsValid()) // Can launch save without save data in mode 13 { std::string fileName = GetFileName(param); std::string filePath = dirPath + "/" + fileName; u8 *data_ = 0; SceSize saveSize = 0; if(cryptedData == 0) // Save decrypted data { saveSize = param->dataSize; if(saveSize == 0 || saveSize > param->dataBufSize) saveSize = param->dataBufSize; // fallback, should never use this data_ = param->dataBuf; } else { data_ = cryptedData; saveSize = cryptedSize; } INFO_LOG(SCEUTILITY,"Saving file with size %u in %s",saveSize,filePath.c_str()); // copy back save name in request strncpy(param->saveName, saveDirName.c_str(), 20); if (fileName == "") { delete[] cryptedData; } else { if (!WritePSPFile(filePath, data_, saveSize)) { ERROR_LOG(SCEUTILITY, "Error writing file %s", filePath.c_str()); delete[] cryptedData; return SCE_UTILITY_SAVEDATA_ERROR_SAVE_MS_NOSPACE; } delete[] cryptedData; } } // SAVE ICON0 if (param->icon0FileData.buf.IsValid()) { std::string icon0path = dirPath + "/" + ICON0_FILENAME; WritePSPFile(icon0path, param->icon0FileData.buf, param->icon0FileData.size); } // SAVE ICON1 if (param->icon1FileData.buf.IsValid()) { std::string icon1path = dirPath + "/" + ICON1_FILENAME; WritePSPFile(icon1path, param->icon1FileData.buf, param->icon1FileData.size); } // SAVE PIC1 if (param->pic1FileData.buf.IsValid()) { std::string pic1path = dirPath + "/" + PIC1_FILENAME; WritePSPFile(pic1path, param->pic1FileData.buf, param->pic1FileData.size); } // Save SND if (param->snd0FileData.buf.IsValid()) { std::string snd0path = dirPath + "/" + SND0_FILENAME; WritePSPFile(snd0path, param->snd0FileData.buf, param->snd0FileData.size); } return 0; } int SavedataParam::Load(SceUtilitySavedataParam *param, const std::string &saveDirName, int saveId, bool secureMode) { if (!param) { return SCE_UTILITY_SAVEDATA_ERROR_LOAD_NO_DATA; } bool isRWMode = param->mode == SCE_UTILITY_SAVEDATA_TYPE_READDATA || param->mode == SCE_UTILITY_SAVEDATA_TYPE_READDATASECURE; std::string dirPath = GetSaveFilePath(param, GetSaveDir(param, saveDirName)); std::string fileName = GetFileName(param); std::string filePath = dirPath + "/" + fileName; std::string sfoPath = dirPath + "/" + SFO_FILENAME; if (!pspFileSystem.GetFileInfo(dirPath).exists) { return isRWMode ? SCE_UTILITY_SAVEDATA_ERROR_RW_NO_DATA : SCE_UTILITY_SAVEDATA_ERROR_LOAD_NO_DATA; } // Load sfo if (!LoadSFO(param, dirPath)) { return isRWMode ? SCE_UTILITY_SAVEDATA_ERROR_RW_DATA_BROKEN : SCE_UTILITY_SAVEDATA_ERROR_LOAD_DATA_BROKEN; } if (fileName != "" && !pspFileSystem.GetFileInfo(filePath).exists) { return isRWMode ? SCE_UTILITY_SAVEDATA_ERROR_RW_FILE_NOT_FOUND : SCE_UTILITY_SAVEDATA_ERROR_LOAD_FILE_NOT_FOUND; } // Don't know what it is, but PSP always respond this and this unlock some game param->bind = 1021; // Load another files, seems these are required by some games, e.g. Fushigi no Dungeon Fuurai no Shiren 4 Plus. // Load ICON0.PNG LoadFile(dirPath, ICON0_FILENAME, ¶m->icon0FileData); // Load ICON1.PNG LoadFile(dirPath, ICON1_FILENAME, ¶m->icon1FileData); // Load PIC1.PNG LoadFile(dirPath, PIC1_FILENAME, ¶m->pic1FileData); // Load SND0.AT3 LoadFile(dirPath, SND0_FILENAME, ¶m->snd0FileData); if (fileName == "") { // Don't load savedata but return success. return 0; } int result = LoadSaveData(param, saveDirName, dirPath, secureMode); if (result != 0) return result; return 0; } int SavedataParam::LoadSaveData(SceUtilitySavedataParam *param, const std::string &saveDirName, const std::string &dirPath, bool secureMode) { if (param->secureVersion > 3) { ERROR_LOG_REPORT(SCEUTILITY, "Savedata version requested: %d", param->secureVersion); return SCE_UTILITY_SAVEDATA_ERROR_LOAD_PARAM; } else if (param->secureVersion != 0) { if (param->secureVersion != 1 && !HasKey(param)) { ERROR_LOG_REPORT(SCEUTILITY, "Savedata version with missing key: %d", param->secureVersion); return SCE_UTILITY_SAVEDATA_ERROR_LOAD_PARAM; } WARN_LOG_REPORT(SCEUTILITY, "Savedata version requested: %d", param->secureVersion); } std::string filename = GetFileName(param); std::string filePath = dirPath + "/" + filename; s64 readSize; INFO_LOG(SCEUTILITY, "Loading file with size %u in %s", param->dataBufSize, filePath.c_str()); u8 *saveData = nullptr; int saveSize = -1; if (!ReadPSPFile(filePath, &saveData, saveSize, &readSize)) { ERROR_LOG(SCEUTILITY,"Error reading file %s",filePath.c_str()); return SCE_UTILITY_SAVEDATA_ERROR_LOAD_NO_DATA; } saveSize = (int)readSize; // copy back save name in request strncpy(param->saveName, saveDirName.c_str(), 20); int prevCryptMode = GetSaveCryptMode(param, saveDirName); bool isCrypted = prevCryptMode != 0 && secureMode; bool saveDone = false; u32 loadedSize = 0; if (isCrypted) { if (DetermineCryptMode(param) > 1 && !HasKey(param)) return SCE_UTILITY_SAVEDATA_ERROR_LOAD_PARAM; u8 hash[16]; bool hasExpectedHash = GetExpectedHash(dirPath, filename, hash); loadedSize = LoadCryptedSave(param, param->dataBuf, saveData, saveSize, prevCryptMode, hasExpectedHash ? hash : nullptr, saveDone); // TODO: Should return SCE_UTILITY_SAVEDATA_ERROR_LOAD_DATA_BROKEN here if !saveDone. } if (!saveDone) { loadedSize = LoadNotCryptedSave(param, param->dataBuf, saveData, saveSize); } param->dataSize = (SceSize)saveSize; delete[] saveData; if (loadedSize != 0) { std::string tag = "LoadSaveData/" + filePath; NotifyMemInfo(MemBlockFlags::WRITE, param->dataBuf.ptr, loadedSize, tag.c_str(), tag.size()); } return 0; } int SavedataParam::DetermineCryptMode(const SceUtilitySavedataParam *param) const { int decryptMode = 1; if (param->secureVersion == 1) { decryptMode = 1; } else if (param->secureVersion == 2) { decryptMode = 3; } else if (param->secureVersion == 3) { decryptMode = GetSDKMainVersion(sceKernelGetCompiledSdkVersion()) >= 4 ? 5 : 1; } else if (HasKey(param)) { // TODO: This should ignore HasKey(), which would trigger errors. Not doing that yet to play it safe. decryptMode = GetSDKMainVersion(sceKernelGetCompiledSdkVersion()) >= 4 ? 5 : 3; } return decryptMode; } u32 SavedataParam::LoadCryptedSave(SceUtilitySavedataParam *param, u8 *data, const u8 *saveData, int &saveSize, int prevCryptMode, const u8 *expectedHash, bool &saveDone) { int orig_size = saveSize; int align_len = align16(saveSize); u8 *data_base = new u8[align_len]; u8 *cryptKey = new u8[0x10]; int decryptMode = DetermineCryptMode(param); const int detectedMode = decryptMode; bool hasKey; auto resetData = [&](int mode) { saveSize = orig_size; align_len = align16(saveSize); hasKey = mode > 1; if (hasKey) { memcpy(cryptKey, param->key, 0x10); } memcpy(data_base, saveData, saveSize); memset(data_base + saveSize, 0, align_len - saveSize); }; resetData(decryptMode); if (decryptMode != prevCryptMode) { if (prevCryptMode == 1 && param->key[0] == 0) { // Backwards compat for a bug we used to have. WARN_LOG(SCEUTILITY, "Savedata loading with hashmode %d instead of detected %d", prevCryptMode, decryptMode); decryptMode = prevCryptMode; // Don't notify the user if we're not going to upgrade the save. if (!g_Config.bEncryptSave) { auto di = GetI18NCategory("Dialog"); host->NotifyUserMessage(di->T("When you save, it will load on a PSP, but not an older PPSSPP"), 6.0f); host->NotifyUserMessage(di->T("Old savedata detected"), 6.0f); } } else { if (decryptMode == 5 && prevCryptMode == 3) { WARN_LOG(SCEUTILITY, "Savedata loading with detected hashmode %d instead of file's %d", decryptMode, prevCryptMode); } else { WARN_LOG_REPORT(SCEUTILITY, "Savedata loading with detected hashmode %d instead of file's %d", decryptMode, prevCryptMode); } if (g_Config.bSavedataUpgrade) { decryptMode = prevCryptMode; auto di = GetI18NCategory("Dialog"); host->NotifyUserMessage(di->T("When you save, it will not work on outdated PSP Firmware anymore"), 6.0f); host->NotifyUserMessage(di->T("Old savedata detected"), 6.0f); } } hasKey = decryptMode > 1; } int err = DecryptSave(decryptMode, data_base, &saveSize, &align_len, hasKey ? cryptKey : nullptr, expectedHash); // Perhaps the file had the wrong mode.... if (err != 0 && detectedMode != decryptMode) { resetData(detectedMode); err = DecryptSave(detectedMode, data_base, &saveSize, &align_len, hasKey ? cryptKey : nullptr, expectedHash); } // TODO: Should return an error, but let's just try with a bad hash. if (err != 0 && expectedHash != nullptr) { WARN_LOG(SCEUTILITY, "Incorrect hash on save data, likely corrupt"); resetData(decryptMode); err = DecryptSave(decryptMode, data_base, &saveSize, &align_len, hasKey ? cryptKey : nullptr, nullptr); } u32 sz = 0; if (err == 0) { if (param->dataBuf.IsValid()) { sz = std::min((u32)saveSize, (u32)param->dataBufSize); memcpy(data, data_base, sz); } saveDone = true; } delete[] data_base; delete[] cryptKey; return sz; } u32 SavedataParam::LoadNotCryptedSave(SceUtilitySavedataParam *param, u8 *data, u8 *saveData, int &saveSize) { if (param->dataBuf.IsValid()) { u32 sz = std::min((u32)saveSize, (u32)param->dataBufSize); memcpy(data, saveData, sz); return sz; } return 0; } bool SavedataParam::LoadSFO(SceUtilitySavedataParam *param, const std::string& dirPath) { ParamSFOData sfoFile; std::string sfopath = dirPath + "/" + SFO_FILENAME; std::vector sfoData; if (pspFileSystem.ReadEntireFile(sfopath, sfoData) >= 0) { sfoFile.ReadSFO(sfoData); // copy back info in request strncpy(param->sfoParam.title,sfoFile.GetValueString("TITLE").c_str(),128); strncpy(param->sfoParam.savedataTitle,sfoFile.GetValueString("SAVEDATA_TITLE").c_str(),128); strncpy(param->sfoParam.detail,sfoFile.GetValueString("SAVEDATA_DETAIL").c_str(),1024); param->sfoParam.parentalLevel = sfoFile.GetValueInt("PARENTAL_LEVEL"); return true; } else { return false; } } std::vector SavedataParam::GetSFOEntries(const std::string &dirPath) { std::vector result; const std::string sfoPath = dirPath + "/" + SFO_FILENAME; ParamSFOData sfoFile; std::vector sfoData; if (pspFileSystem.ReadEntireFile(sfoPath, sfoData) >= 0) { sfoFile.ReadSFO(sfoData); } else { return result; } const int FILE_LIST_COUNT_MAX = 99; u32 sfoFileListSize = 0; SaveSFOFileListEntry *sfoFileList = (SaveSFOFileListEntry *)sfoFile.GetValueData("SAVEDATA_FILE_LIST", &sfoFileListSize); const u32 count = std::min((u32)FILE_LIST_COUNT_MAX, sfoFileListSize / (u32)sizeof(SaveSFOFileListEntry)); for (u32 i = 0; i < count; ++i) { if (sfoFileList[i].filename[0] != '\0') result.push_back(sfoFileList[i]); } return result; } std::set SavedataParam::GetSecureFileNames(const std::string &dirPath) { auto entries = GetSFOEntries(dirPath); std::set secureFileNames; for (auto entry : entries) { char temp[14]; truncate_cpy(temp, entry.filename); secureFileNames.insert(temp); } return secureFileNames; } bool SavedataParam::GetExpectedHash(const std::string &dirPath, const std::string &filename, u8 hash[16]) { auto entries = GetSFOEntries(dirPath); for (auto entry : entries) { if (strncmp(entry.filename, filename.c_str(), sizeof(entry.filename)) == 0) { memcpy(hash, entry.hash, sizeof(entry.hash)); return true; } } return false; } void SavedataParam::LoadFile(const std::string& dirPath, const std::string& filename, PspUtilitySavedataFileData *fileData) { std::string filePath = dirPath + "/" + filename; if (!fileData->buf.IsValid()) return; u8 *buf = fileData->buf; u32 size = Memory::ValidSize(fileData->buf.ptr, fileData->bufSize); s64 readSize = -1; if (ReadPSPFile(filePath, &buf, size, &readSize)) { fileData->size = readSize; const std::string tag = "SavedataLoad/" + filePath; NotifyMemInfo(MemBlockFlags::WRITE, fileData->buf.ptr, fileData->size, tag.c_str(), tag.size()); } } int SavedataParam::EncryptData(unsigned int mode, unsigned char *data, int *dataLen, int *alignedLen, unsigned char *hash, unsigned char *cryptkey) { pspChnnlsvContext1 ctx1; pspChnnlsvContext2 ctx2; /* Make room for the IV in front of the data. */ memmove(data + 0x10, data, *alignedLen); /* Set up buffers */ memset(&ctx1, 0, sizeof(pspChnnlsvContext1)); memset(&ctx2, 0, sizeof(pspChnnlsvContext2)); memset(hash, 0, 0x10); memset(data, 0, 0x10); /* Build the 0x10-byte IV and setup encryption */ if (sceSdCreateList_(ctx2, mode, 1, data, cryptkey) < 0) return -1; if (sceSdSetIndex_(ctx1, mode) < 0) return -2; if (sceSdRemoveValue_(ctx1, data, 0x10) < 0) return -3; if (sceSdSetMember_(ctx2, data + 0x10, *alignedLen) < 0) return -4; /* Clear any extra bytes left from the previous steps */ memset(data + 0x10 + *dataLen, 0, *alignedLen - *dataLen); /* Encrypt the data */ if (sceSdRemoveValue_(ctx1, data + 0x10, *alignedLen) < 0) return -5; /* Verify encryption */ if (sceChnnlsv_21BE78B4_(ctx2) < 0) return -6; /* Build the file hash from this PSP */ if (sceSdGetLastIndex_(ctx1, hash, cryptkey) < 0) return -7; /* Adjust sizes to account for IV */ *alignedLen += 0x10; *dataLen += 0x10; /* All done */ return 0; } int SavedataParam::DecryptSave(unsigned int mode, unsigned char *data, int *dataLen, int *alignedLen, unsigned char *cryptkey, const u8 *expectedHash) { pspChnnlsvContext1 ctx1; pspChnnlsvContext2 ctx2; /* Need a 16-byte IV plus some data */ if (*alignedLen <= 0x10) return -1; *dataLen -= 0x10; *alignedLen -= 0x10; /* Set up buffers */ memset(&ctx1, 0, sizeof(pspChnnlsvContext1)); memset(&ctx2, 0, sizeof(pspChnnlsvContext2)); /* Perform the magic */ if (sceSdSetIndex_(ctx1, mode) < 0) return -2; if (sceSdCreateList_(ctx2, mode, 2, data, cryptkey) < 0) return -3; if (sceSdRemoveValue_(ctx1, data, 0x10) < 0) return -4; if (sceSdRemoveValue_(ctx1, data + 0x10, *alignedLen) < 0) return -5; if (sceSdSetMember_(ctx2, data + 0x10, *alignedLen) < 0) return -6; /* Verify that it decrypted correctly */ if (sceChnnlsv_21BE78B4_(ctx2) < 0) return -7; if (expectedHash) { u8 hash[16]; if (sceSdGetLastIndex_(ctx1, hash, cryptkey) < 0) return -7; if (memcmp(hash, expectedHash, sizeof(hash)) != 0) return -8; } /* The decrypted data starts at data + 0x10, so shift it back. */ memmove(data, data + 0x10, *dataLen); return 0; } int SavedataParam::UpdateHash(u8* sfoData, int sfoSize, int sfoDataParamsOffset, int encryptmode) { int alignedLen = align16(sfoSize); memset(sfoData+sfoDataParamsOffset, 0, 128); u8 filehash[16]; int ret = 0; int firstHashMode = encryptmode & 2 ? 4 : 2; int secondHashMode = encryptmode & 2 ? 3 : 0; if (encryptmode & 4) { firstHashMode = 6; secondHashMode = 5; } // Compute 11D0 hash over entire file if ((ret = BuildHash(filehash, sfoData, sfoSize, alignedLen, firstHashMode, 0)) < 0) { // Not sure about "2" return ret - 400; } // Copy 11D0 hash to param.sfo and set flag indicating it's there memcpy(sfoData+sfoDataParamsOffset + 0x20, filehash, 0x10); *(sfoData+sfoDataParamsOffset) |= 0x01; // If new encryption mode, compute and insert the 1220 hash. if (encryptmode & 6) { /* Enable the hash bit first */ *(sfoData+sfoDataParamsOffset) |= (encryptmode & 6) << 4; if ((ret = BuildHash(filehash, sfoData, sfoSize, alignedLen, secondHashMode, 0)) < 0) { return ret - 500; } memcpy(sfoData+sfoDataParamsOffset + 0x70, filehash, 0x10); } /* Compute and insert the 11C0 hash. */ if ((ret = BuildHash(filehash, sfoData, sfoSize, alignedLen, 1, 0)) < 0) { return ret - 600; } memcpy(sfoData+sfoDataParamsOffset + 0x10, filehash, 0x10); /* All done. */ return 0; } int SavedataParam::BuildHash(unsigned char *output, unsigned char *data, unsigned int len, unsigned int alignedLen, int mode, unsigned char *cryptkey) { pspChnnlsvContext1 ctx1; /* Set up buffers */ memset(&ctx1, 0, sizeof(pspChnnlsvContext1)); memset(output, 0, 0x10); memset(data + len, 0, alignedLen - len); /* Perform the magic */ if (sceSdSetIndex_(ctx1, mode & 0xFF) < 0) return -1; if (sceSdRemoveValue_(ctx1, data, alignedLen) < 0) return -2; if (sceSdGetLastIndex_(ctx1, output, cryptkey) < 0) { // Got here since Kirk CMD5 missing, return random value; memset(output,0x1,0x10); return 0; } /* All done. */ return 0; } // TODO: Merge with NiceSizeFormat? That one has a decimal though. std::string SavedataParam::GetSpaceText(u64 size, bool roundUp) { char text[50]; static const char *suffixes[] = {"B", "KB", "MB", "GB"}; for (size_t i = 0; i < ARRAY_SIZE(suffixes); ++i) { if (size < 1024) { snprintf(text, sizeof(text), "%lld %s", size, suffixes[i]); return std::string(text); } if (roundUp) { size = (size + 1023) / 1024; } else { size /= 1024; } } snprintf(text, sizeof(text), "%llu TB", size); return std::string(text); } int SavedataParam::GetSizes(SceUtilitySavedataParam *param) { if (!param) { return SCE_UTILITY_SAVEDATA_ERROR_SIZES_NO_DATA; } int ret = 0; if (param->msFree.IsValid()) { const u64 freeBytes = MemoryStick_FreeSpace(); param->msFree->clusterSize = (u32)MemoryStick_SectorSize(); param->msFree->freeClusters = (u32)(freeBytes / MemoryStick_SectorSize()); param->msFree->freeSpaceKB = (u32)(freeBytes / 0x400); const std::string spaceTxt = SavedataParam::GetSpaceText(freeBytes, false); memset(param->msFree->freeSpaceStr, 0, sizeof(param->msFree->freeSpaceStr)); strncpy(param->msFree->freeSpaceStr, spaceTxt.c_str(), sizeof(param->msFree->freeSpaceStr)); NotifyMemInfo(MemBlockFlags::WRITE, param->msFree.ptr, sizeof(SceUtilitySavedataMsFreeInfo), "SavedataGetSizes"); } if (param->msData.IsValid()) { const SceUtilitySavedataMsDataInfo *msData = param->msData; const std::string gameName(msData->gameName, strnlen(msData->gameName, sizeof(msData->gameName))); const std::string saveName(msData->saveName, strnlen(msData->saveName, sizeof(msData->saveName))); // TODO: How should <> be handled? std::string path = GetSaveFilePath(param, gameName + (saveName == "<>" ? "" : saveName)); PSPFileInfo finfo = pspFileSystem.GetFileInfo(path); if (finfo.exists) { param->msData->info.usedClusters = 0; auto listing = pspFileSystem.GetDirListing(path); for (auto it = listing.begin(), end = listing.end(); it != end; ++it) { param->msData->info.usedClusters += (it->size + (u32)MemoryStick_SectorSize() - 1) / (u32)MemoryStick_SectorSize(); } // The usedSpaceKB value is definitely based on clusters, not bytes or even KB. // Fieldrunners expects 736 KB, even though the files add up to ~600 KB. int total_size = param->msData->info.usedClusters * (u32)MemoryStick_SectorSize(); param->msData->info.usedSpaceKB = total_size / 0x400; std::string spaceTxt = SavedataParam::GetSpaceText(total_size, true); strncpy(param->msData->info.usedSpaceStr, spaceTxt.c_str(), sizeof(param->msData->info.usedSpaceStr)); // TODO: What does this mean, then? Seems to be the same. param->msData->info.usedSpace32KB = param->msData->info.usedSpaceKB; strncpy(param->msData->info.usedSpace32Str, spaceTxt.c_str(), sizeof(param->msData->info.usedSpace32Str)); } else { param->msData->info.usedClusters = 0; param->msData->info.usedSpaceKB = 0; strncpy(param->msData->info.usedSpaceStr, "", sizeof(param->msData->info.usedSpaceStr)); param->msData->info.usedSpace32KB = 0; strncpy(param->msData->info.usedSpace32Str, "", sizeof(param->msData->info.usedSpace32Str)); ret = SCE_UTILITY_SAVEDATA_ERROR_SIZES_NO_DATA; } NotifyMemInfo(MemBlockFlags::WRITE, param->msData.ptr, sizeof(SceUtilitySavedataMsDataInfo), "SavedataGetSizes"); } if (param->utilityData.IsValid()) { int total_size = 0; // The directory record itself. // TODO: Account for number of files / actual record size? total_size += getSizeNormalized(1); // Account for the SFO (is this always 1 sector?) total_size += getSizeNormalized(1); // Add the size of the data itself (don't forget encryption overhead.) // This is only added if a filename is specified. if (param->fileName[0] != 0) { if (g_Config.bEncryptSave) { total_size += getSizeNormalized((u32)param->dataSize + 16); } else { total_size += getSizeNormalized((u32)param->dataSize); } } total_size += getSizeNormalized(param->icon0FileData.size); total_size += getSizeNormalized(param->icon1FileData.size); total_size += getSizeNormalized(param->pic1FileData.size); total_size += getSizeNormalized(param->snd0FileData.size); param->utilityData->usedClusters = total_size / (u32)MemoryStick_SectorSize(); param->utilityData->usedSpaceKB = total_size / 0x400; std::string spaceTxt = SavedataParam::GetSpaceText(total_size, true); memset(param->utilityData->usedSpaceStr, 0, sizeof(param->utilityData->usedSpaceStr)); strncpy(param->utilityData->usedSpaceStr, spaceTxt.c_str(), sizeof(param->utilityData->usedSpaceStr)); // TODO: Maybe these are rounded to the nearest 32KB? Or something? param->utilityData->usedSpace32KB = total_size / 0x400; spaceTxt = SavedataParam::GetSpaceText(total_size, true); memset(param->utilityData->usedSpace32Str, 0, sizeof(param->utilityData->usedSpace32Str)); strncpy(param->utilityData->usedSpace32Str, spaceTxt.c_str(), sizeof(param->utilityData->usedSpace32Str)); NotifyMemInfo(MemBlockFlags::WRITE, param->utilityData.ptr, sizeof(SceUtilitySavedataUsedDataInfo), "SavedataGetSizes"); } return ret; } bool SavedataParam::GetList(SceUtilitySavedataParam *param) { if (!param) { return false; } if (param->idList.IsValid()) { u32 maxFile = param->idList->maxCount; std::vector validDir; std::vector sfoFiles; std::vector allDir = pspFileSystem.GetDirListing(savePath); std::string searchString = GetGameName(param)+GetSaveName(param); for (size_t i = 0; i < allDir.size() && validDir.size() < maxFile; i++) { std::string dirName = allDir[i].name; if(PSPMatch(dirName, searchString)) { validDir.push_back(allDir[i]); } } PSPFileInfo sfoFile; for (size_t i = 0; i < validDir.size(); ++i) { // GetFileName(param) == NUll here // so use sfo files to set the date. sfoFile = pspFileSystem.GetFileInfo(savePath + validDir[i].name + "/" + SFO_FILENAME); sfoFiles.push_back(sfoFile); } SceUtilitySavedataIdListEntry *entries = param->idList->entries; for (u32 i = 0; i < (u32)validDir.size(); i++) { entries[i].st_mode = 0x11FF; if (sfoFiles[i].exists) { __IoCopyDate(entries[i].st_ctime, sfoFiles[i].ctime); __IoCopyDate(entries[i].st_atime, sfoFiles[i].atime); __IoCopyDate(entries[i].st_mtime, sfoFiles[i].mtime); } else { __IoCopyDate(entries[i].st_ctime, validDir[i].ctime); __IoCopyDate(entries[i].st_atime, validDir[i].atime); __IoCopyDate(entries[i].st_mtime, validDir[i].mtime); } // folder name without gamename (max 20 u8) std::string outName = validDir[i].name.substr(GetGameName(param).size()); memset(entries[i].name, 0, sizeof(entries[i].name)); strncpy(entries[i].name, outName.c_str(), sizeof(entries[i].name)); } // Save num of folder found param->idList->resultCount = (u32)validDir.size(); NotifyMemInfo(MemBlockFlags::WRITE, param->idList.ptr, sizeof(SceUtilitySavedataIdListInfo), "SavedataGetList"); NotifyMemInfo(MemBlockFlags::WRITE, param->idList->entries.ptr, (uint32_t)validDir.size() * sizeof(SceUtilitySavedataIdListEntry), "SavedataGetList"); } return true; } int SavedataParam::GetFilesList(SceUtilitySavedataParam *param) { if (!param) { return SCE_UTILITY_SAVEDATA_ERROR_RW_BAD_STATUS; } if (!param->fileList.IsValid()) { ERROR_LOG_REPORT(SCEUTILITY, "SavedataParam::GetFilesList(): bad fileList address %08x", param->fileList.ptr); // Should crash. return -1; } auto &fileList = param->fileList; if (fileList->secureEntries.IsValid() && fileList->maxSecureEntries > 99) { ERROR_LOG_REPORT(SCEUTILITY, "SavedataParam::GetFilesList(): too many secure entries, %d", fileList->maxSecureEntries); return SCE_UTILITY_SAVEDATA_ERROR_RW_BAD_PARAMS; } if (fileList->normalEntries.IsValid() && fileList->maxNormalEntries > 8192) { ERROR_LOG_REPORT(SCEUTILITY, "SavedataParam::GetFilesList(): too many normal entries, %d", fileList->maxNormalEntries); return SCE_UTILITY_SAVEDATA_ERROR_RW_BAD_PARAMS; } if (sceKernelGetCompiledSdkVersion() >= 0x02060000) { if (fileList->systemEntries.IsValid() && fileList->maxSystemEntries > 5) { ERROR_LOG_REPORT(SCEUTILITY, "SavedataParam::GetFilesList(): too many system entries, %d", fileList->maxSystemEntries); return SCE_UTILITY_SAVEDATA_ERROR_RW_BAD_PARAMS; } } std::string dirPath = savePath + GetGameName(param) + GetSaveName(param); if (!pspFileSystem.GetFileInfo(dirPath).exists) { DEBUG_LOG(SCEUTILITY, "SavedataParam::GetFilesList(): directory %s does not exist", dirPath.c_str()); return SCE_UTILITY_SAVEDATA_ERROR_RW_NO_DATA; } // Even if there are no files, initialize to 0. fileList->resultNumSecureEntries = 0; fileList->resultNumNormalEntries = 0; fileList->resultNumSystemEntries = 0; // We need PARAM.SFO's SAVEDATA_FILE_LIST to determine which entries are secure. PSPFileInfo sfoFileInfo = pspFileSystem.GetFileInfo(dirPath + "/" + SFO_FILENAME); std::set secureFilenames; if (sfoFileInfo.exists) { secureFilenames = GetSecureFileNames(dirPath); } else { return SCE_UTILITY_SAVEDATA_ERROR_RW_DATA_BROKEN; } // Does not list directories, nor recurse into them, and ignores files not ALL UPPERCASE. auto files = pspFileSystem.GetDirListing(dirPath); for (auto file = files.begin(), end = files.end(); file != end; ++file) { if (file->type == FILETYPE_DIRECTORY) { continue; } // TODO: What are the exact rules? It definitely skips lowercase, and allows FILE or FILE.EXT. if (file->name.find_first_of("abcdefghijklmnopqrstuvwxyz") != file->name.npos) { DEBUG_LOG(SCEUTILITY, "SavedataParam::GetFilesList(): skipping file %s with lowercase", file->name.c_str()); continue; } bool isSystemFile = file->name == ICON0_FILENAME || file->name == ICON1_FILENAME || file->name == PIC1_FILENAME; isSystemFile = isSystemFile || file->name == SND0_FILENAME || file->name == SFO_FILENAME; SceUtilitySavedataFileListEntry *entry = NULL; int sizeOffset = 0; if (isSystemFile) { if (fileList->systemEntries.IsValid() && fileList->resultNumSystemEntries < fileList->maxSystemEntries) { entry = &fileList->systemEntries[fileList->resultNumSystemEntries++]; } } else if (secureFilenames.find(file->name) != secureFilenames.end()) { if (fileList->secureEntries.IsValid() && fileList->resultNumSecureEntries < fileList->maxSecureEntries) { entry = &fileList->secureEntries[fileList->resultNumSecureEntries++]; } // Secure files are slightly bigger. bool isCrypted = GetSaveCryptMode(param, GetSaveDirName(param, 0)) != 0; if (isCrypted) { sizeOffset = -0x10; } } else { if (fileList->normalEntries.IsValid() && fileList->resultNumNormalEntries < fileList->maxNormalEntries) { entry = &fileList->normalEntries[fileList->resultNumNormalEntries++]; } } // Out of space for this file in the list. if (entry == NULL) { continue; } entry->st_mode = 0x21FF; entry->st_size = file->size + sizeOffset; __IoCopyDate(entry->st_ctime, file->ctime); __IoCopyDate(entry->st_atime, file->atime); __IoCopyDate(entry->st_mtime, file->mtime); // TODO: Probably actually 13 + 3 pad... strncpy(entry->name, file->name.c_str(), 16); entry->name[15] = '\0'; } // TODO: Does this always happen? // Don't know what it is, but PSP always respond this param->bind = 1021; NotifyMemInfo(MemBlockFlags::WRITE, fileList.ptr, sizeof(SceUtilitySavedataFileListInfo), "SavedataGetFilesList"); if (fileList->resultNumSystemEntries != 0) NotifyMemInfo(MemBlockFlags::WRITE, fileList->systemEntries.ptr, fileList->resultNumSystemEntries * sizeof(SceUtilitySavedataFileListEntry), "SavedataGetFilesList"); if (fileList->resultNumSecureEntries != 0) NotifyMemInfo(MemBlockFlags::WRITE, fileList->secureEntries.ptr, fileList->resultNumSecureEntries * sizeof(SceUtilitySavedataFileListEntry), "SavedataGetFilesList"); if (fileList->resultNumNormalEntries != 0) NotifyMemInfo(MemBlockFlags::WRITE, fileList->normalEntries.ptr, fileList->resultNumNormalEntries * sizeof(SceUtilitySavedataFileListEntry), "SavedataGetFilesList"); return 0; } bool SavedataParam::GetSize(SceUtilitySavedataParam *param) { if (!param) { return false; } const std::string saveDir = savePath + GetGameName(param) + GetSaveName(param); PSPFileInfo info = pspFileSystem.GetFileInfo(saveDir); bool exists = info.exists; if (param->sizeInfo.IsValid()) { const u64 freeBytes = MemoryStick_FreeSpace(); s64 overwriteBytes = 0; s64 writeBytes = 0; for (int i = 0; i < param->sizeInfo->numNormalEntries; ++i) { const auto &entry = param->sizeInfo->normalEntries[i]; overwriteBytes += pspFileSystem.GetFileInfo(saveDir + "/" + entry.name).size; writeBytes += entry.size; } for (int i = 0; i < param->sizeInfo->numSecureEntries; ++i) { const auto &entry = param->sizeInfo->secureEntries[i]; overwriteBytes += pspFileSystem.GetFileInfo(saveDir + "/" + entry.name).size; writeBytes += entry.size + 0x10; } param->sizeInfo->sectorSize = (int)MemoryStick_SectorSize(); param->sizeInfo->freeSectors = (int)(freeBytes / MemoryStick_SectorSize()); // TODO: Is this after the specified files? Probably before? param->sizeInfo->freeKB = (int)(freeBytes / 1024); std::string spaceTxt = SavedataParam::GetSpaceText(freeBytes, false); truncate_cpy(param->sizeInfo->freeString, spaceTxt.c_str()); if (writeBytes - overwriteBytes < (s64)freeBytes) { param->sizeInfo->neededKB = 0; // Note: this is "needed to overwrite". param->sizeInfo->overwriteKB = 0; spaceTxt = GetSpaceText(0, true); truncate_cpy(param->sizeInfo->neededString, spaceTxt.c_str()); truncate_cpy(param->sizeInfo->overwriteString, spaceTxt.c_str()); } else { // Bytes needed to save additional data. s64 neededBytes = writeBytes - freeBytes; param->sizeInfo->neededKB = (neededBytes + 1023) / 1024; spaceTxt = GetSpaceText(neededBytes, true); truncate_cpy(param->sizeInfo->neededString, spaceTxt.c_str()); if (writeBytes - overwriteBytes < (s64)freeBytes) { param->sizeInfo->overwriteKB = 0; spaceTxt = GetSpaceText(0, true); truncate_cpy(param->sizeInfo->overwriteString, spaceTxt.c_str()); } else { s64 neededOverwriteBytes = writeBytes - freeBytes - overwriteBytes; param->sizeInfo->overwriteKB = (neededOverwriteBytes + 1023) / 1024; spaceTxt = GetSpaceText(neededOverwriteBytes, true); truncate_cpy(param->sizeInfo->overwriteString, spaceTxt.c_str()); } } NotifyMemInfo(MemBlockFlags::WRITE, param->sizeInfo.ptr, sizeof(PspUtilitySavedataSizeInfo), "SavedataGetSize"); } return exists; } void SavedataParam::Clear() { if (saveDataList) { for (int i = 0; i < saveNameListDataCount; i++) { if (saveDataList[i].texture != NULL && (!noSaveIcon || saveDataList[i].texture != noSaveIcon->texture)) delete saveDataList[i].texture; saveDataList[i].texture = NULL; } delete [] saveDataList; saveDataList = 0; saveDataListCount = 0; } if (noSaveIcon) { if (noSaveIcon->texture != NULL) delete noSaveIcon->texture; noSaveIcon->texture = NULL; delete noSaveIcon; noSaveIcon = 0; } } int SavedataParam::SetPspParam(SceUtilitySavedataParam *param) { pspParam = param; if (!pspParam) { Clear(); return 0; } if (param->mode == SCE_UTILITY_SAVEDATA_TYPE_LISTALLDELETE) { Clear(); int realCount = 0; auto allSaves = pspFileSystem.GetDirListing(savePath); saveDataListCount = (int)allSaves.size(); saveDataList = new SaveFileInfo[saveDataListCount]; for (auto save : allSaves) { if (save.type != FILETYPE_DIRECTORY || save.name == "." || save.name == "..") continue; std::string fileDataDir = savePath + save.name; PSPFileInfo info = GetSaveInfo(fileDataDir); SetFileInfo(realCount, info, "", save.name); realCount++; } saveNameListDataCount = realCount; return 0; } bool listEmptyFile = true; if (param->mode == SCE_UTILITY_SAVEDATA_TYPE_LISTLOAD || param->mode == SCE_UTILITY_SAVEDATA_TYPE_LISTDELETE) { listEmptyFile = false; } SceUtilitySavedataSaveName *saveNameListData; bool hasMultipleFileName = false; if (param->saveNameList.IsValid()) { Clear(); saveNameListData = param->saveNameList; // Get number of fileName in array saveDataListCount = 0; while (saveNameListData[saveDataListCount][0] != 0) { saveDataListCount++; } if (saveDataListCount > 0 && wouldHasMultiSaveName(param)) { hasMultipleFileName = true; saveDataList = new SaveFileInfo[saveDataListCount]; // get and stock file info for each file int realCount = 0; for (int i = 0; i < saveDataListCount; i++) { // "<>" means saveName can be anything... if (strncmp(saveNameListData[i], "<>", ARRAY_SIZE(saveNameListData[i])) == 0) { std::string fileDataPath = ""; // TODO:Maybe we need a way to reorder the files? auto allSaves = pspFileSystem.GetDirListing(savePath); std::string gameName = GetGameName(param); std::string saveName = ""; for(auto it = allSaves.begin(); it != allSaves.end(); ++it) { if(it->name.compare(0, gameName.length(), gameName) == 0) { saveName = it->name.substr(gameName.length()); if(IsInSaveDataList(saveName, realCount)) // Already in SaveDataList, skip... continue; fileDataPath = savePath + it->name; PSPFileInfo info = pspFileSystem.GetFileInfo(fileDataPath); if (info.exists) { SetFileInfo(realCount, info, saveName); DEBUG_LOG(SCEUTILITY,"%s Exist",fileDataPath.c_str()); ++realCount; } else { if (listEmptyFile) { // If file doesn't exist,we only skip... continue; } } break; } } continue; } const std::string thisSaveName = FixedToString(saveNameListData[i], ARRAY_SIZE(saveNameListData[i])); std::string fileDataDir = savePath + GetGameName(param) + thisSaveName; PSPFileInfo info = GetSaveInfo(fileDataDir); if (info.exists) { SetFileInfo(realCount, info, thisSaveName); DEBUG_LOG(SCEUTILITY, "Save data exists: %s = %s", thisSaveName.c_str(), fileDataDir.c_str()); realCount++; } else { if (listEmptyFile) { ClearFileInfo(saveDataList[realCount], thisSaveName); DEBUG_LOG(SCEUTILITY, "Listing missing save data: %s = %s", thisSaveName.c_str(), fileDataDir.c_str()); realCount++; } else { DEBUG_LOG(SCEUTILITY, "Save data not found: %s = %s", thisSaveName.c_str(), fileDataDir.c_str()); } } } saveNameListDataCount = realCount; } } // Load info on only save if (!hasMultipleFileName) { saveNameListData = 0; Clear(); saveDataList = new SaveFileInfo[1]; saveDataListCount = 1; // get and stock file info for each file std::string fileDataDir = savePath + GetGameName(param) + GetSaveName(param); PSPFileInfo info = GetSaveInfo(fileDataDir); if (info.exists) { SetFileInfo(0, info, GetSaveName(param)); DEBUG_LOG(SCEUTILITY, "Save data exists: %s = %s", GetSaveName(param).c_str(), fileDataDir.c_str()); saveNameListDataCount = 1; } else { if (listEmptyFile) { ClearFileInfo(saveDataList[0], GetSaveName(param)); DEBUG_LOG(SCEUTILITY, "Listing missing save data: %s = %s", GetSaveName(param).c_str(), fileDataDir.c_str()); } else { DEBUG_LOG(SCEUTILITY, "Save data not found: %s = %s", GetSaveName(param).c_str(), fileDataDir.c_str()); } saveNameListDataCount = 0; return 0; } } return 0; } void SavedataParam::SetFileInfo(SaveFileInfo &saveInfo, PSPFileInfo &info, std::string saveName, std::string savrDir) { saveInfo.size = info.size; saveInfo.saveName = saveName; saveInfo.idx = 0; saveInfo.modif_time = info.mtime; std::string saveDir = savrDir == "" ? GetGameName(pspParam) + saveName : savrDir; saveInfo.saveDir = saveDir; // Start with a blank slate. if (saveInfo.texture != NULL) { if (!noSaveIcon || saveInfo.texture != noSaveIcon->texture) { delete saveInfo.texture; } saveInfo.texture = NULL; } saveInfo.title[0] = 0; saveInfo.saveTitle[0] = 0; saveInfo.saveDetail[0] = 0; // Search save image icon0 // TODO : If icon0 don't exist, need to use icon1 which is a moving icon. Also play sound std::string fileDataPath2 = savePath + saveDir + "/" + ICON0_FILENAME; PSPFileInfo info2 = pspFileSystem.GetFileInfo(fileDataPath2); if (info2.exists) saveInfo.texture = new PPGeImage(fileDataPath2); // Load info in PARAM.SFO fileDataPath2 = savePath + saveDir + "/" + SFO_FILENAME; info2 = pspFileSystem.GetFileInfo(fileDataPath2); if (info2.exists) { std::vector sfoData; pspFileSystem.ReadEntireFile(fileDataPath2, sfoData); ParamSFOData sfoFile; if (sfoFile.ReadSFO(sfoData)) { SetStringFromSFO(sfoFile, "TITLE", saveInfo.title, sizeof(saveInfo.title)); SetStringFromSFO(sfoFile, "SAVEDATA_TITLE", saveInfo.saveTitle, sizeof(saveInfo.saveTitle)); SetStringFromSFO(sfoFile, "SAVEDATA_DETAIL", saveInfo.saveDetail, sizeof(saveInfo.saveDetail)); } } else { saveInfo.broken = true; truncate_cpy(saveInfo.title, saveDir.c_str()); } } void SavedataParam::SetFileInfo(int idx, PSPFileInfo &info, std::string saveName, std::string saveDir) { SetFileInfo(saveDataList[idx], info, saveName, saveDir); saveDataList[idx].idx = idx; } void SavedataParam::ClearFileInfo(SaveFileInfo &saveInfo, const std::string &saveName) { saveInfo.size = 0; saveInfo.saveName = saveName; saveInfo.idx = 0; saveInfo.broken = false; if (saveInfo.texture != NULL) { if (!noSaveIcon || saveInfo.texture != noSaveIcon->texture) { delete saveInfo.texture; } saveInfo.texture = NULL; } if (GetPspParam()->newData.IsValid() && GetPspParam()->newData->buf.IsValid()) { // We have a png to show if (!noSaveIcon) { noSaveIcon = new SaveFileInfo(); PspUtilitySavedataFileData *newData = GetPspParam()->newData; noSaveIcon->texture = new PPGeImage(newData->buf.ptr, (SceSize)newData->size); } saveInfo.texture = noSaveIcon->texture; } else if ((u32)GetPspParam()->mode == SCE_UTILITY_SAVEDATA_TYPE_SAVE && GetPspParam()->icon0FileData.buf.IsValid()) { const PspUtilitySavedataFileData &icon0FileData = GetPspParam()->icon0FileData; saveInfo.texture = new PPGeImage(icon0FileData.buf.ptr, (SceSize)icon0FileData.size); } } PSPFileInfo SavedataParam::GetSaveInfo(std::string saveDir) { PSPFileInfo info = pspFileSystem.GetFileInfo(saveDir); if (info.exists) { info.access = 0777; auto allFiles = pspFileSystem.GetDirListing(saveDir); bool firstFile = true; for (auto file : allFiles) { if (file.type == FILETYPE_DIRECTORY || file.name == "." || file.name == "..") continue; // Use a file to determine save date. if (firstFile) { info.ctime = file.ctime; info.mtime = file.mtime; info.atime = file.atime; info.size += file.size; firstFile = false; } else { info.size += file.size; } } } return info; } SceUtilitySavedataParam *SavedataParam::GetPspParam() { return pspParam; } const SceUtilitySavedataParam *SavedataParam::GetPspParam() const { return pspParam; } int SavedataParam::GetFilenameCount() { return saveNameListDataCount; } const SaveFileInfo& SavedataParam::GetFileInfo(int idx) { return saveDataList[idx]; } std::string SavedataParam::GetFilename(int idx) const { return saveDataList[idx].saveName; } std::string SavedataParam::GetSaveDir(int idx) const { return saveDataList[idx].saveDir; } int SavedataParam::GetSelectedSave() { // The slot # of the same save on LOAD/SAVE lists can dismatch so this isn't right anyhow return selectedSave < saveNameListDataCount ? selectedSave : 0; } void SavedataParam::SetSelectedSave(int idx) { selectedSave = idx; } int SavedataParam::GetFirstListSave() { return 0; } int SavedataParam::GetLastListSave() { return saveNameListDataCount - 1; } int SavedataParam::GetLatestSave() { int idx = 0; time_t idxTime = 0; for (int i = 0; i < saveNameListDataCount; ++i) { if (saveDataList[i].size == 0) continue; time_t thisTime = mktime(&saveDataList[i].modif_time); if ((s64)idxTime < (s64)thisTime) { idx = i; idxTime = thisTime; } } return idx; } int SavedataParam::GetOldestSave() { int idx = 0; time_t idxTime = 0; for (int i = 0; i < saveNameListDataCount; ++i) { if (saveDataList[i].size == 0) continue; time_t thisTime = mktime(&saveDataList[i].modif_time); if ((s64)idxTime > (s64)thisTime) { idx = i; idxTime = thisTime; } } return idx; } int SavedataParam::GetFirstDataSave() { int idx = 0; for (int i = 0; i < saveNameListDataCount; ++i) { if (saveDataList[i].size != 0) { idx = i; break; } } return idx; } int SavedataParam::GetLastDataSave() { int idx = 0; for (int i = saveNameListDataCount; i > 0; ) { --i; if (saveDataList[i].size != 0) { idx = i; break; } } return idx; } int SavedataParam::GetFirstEmptySave() { int idx = 0; for (int i = 0; i < saveNameListDataCount; ++i) { if (saveDataList[i].size == 0) { idx = i; break; } } return idx; } int SavedataParam::GetLastEmptySave() { int idx = 0; for (int i = saveNameListDataCount; i > 0; ) { --i; if (saveDataList[i].size == 0) { idx = i; break; } } return idx; } int SavedataParam::GetSaveNameIndex(SceUtilitySavedataParam* param) { std::string saveName = GetSaveName(param); for (int i = 0; i < saveNameListDataCount; i++) { // TODO: saveName may contain wildcards if (saveDataList[i].saveName == saveName) { return i; } } return 0; } bool SavedataParam::wouldHasMultiSaveName(SceUtilitySavedataParam* param) { switch ((SceUtilitySavedataType)(u32)param->mode) { case SCE_UTILITY_SAVEDATA_TYPE_LOAD: case SCE_UTILITY_SAVEDATA_TYPE_AUTOLOAD: case SCE_UTILITY_SAVEDATA_TYPE_SAVE: case SCE_UTILITY_SAVEDATA_TYPE_AUTOSAVE: case SCE_UTILITY_SAVEDATA_TYPE_MAKEDATASECURE: case SCE_UTILITY_SAVEDATA_TYPE_MAKEDATA: case SCE_UTILITY_SAVEDATA_TYPE_READDATASECURE: case SCE_UTILITY_SAVEDATA_TYPE_READDATA: case SCE_UTILITY_SAVEDATA_TYPE_WRITEDATASECURE: case SCE_UTILITY_SAVEDATA_TYPE_WRITEDATA: case SCE_UTILITY_SAVEDATA_TYPE_AUTODELETE: case SCE_UTILITY_SAVEDATA_TYPE_DELETE: case SCE_UTILITY_SAVEDATA_TYPE_ERASESECURE: case SCE_UTILITY_SAVEDATA_TYPE_ERASE: case SCE_UTILITY_SAVEDATA_TYPE_DELETEDATA: return false; default: return true; } } void SavedataParam::DoState(PointerWrap &p) { auto s = p.Section("SavedataParam", 1); if (!s) return; // pspParam is handled in PSPSaveDialog. Do(p, selectedSave); Do(p, saveDataListCount); Do(p, saveNameListDataCount); if (p.mode == p.MODE_READ) { if (saveDataList != NULL) delete [] saveDataList; if (saveDataListCount != 0) { saveDataList = new SaveFileInfo[saveDataListCount]; DoArray(p, saveDataList, saveDataListCount); } else saveDataList = NULL; } else DoArray(p, saveDataList, saveDataListCount); } int SavedataParam::GetSaveCryptMode(SceUtilitySavedataParam* param, const std::string &saveDirName) { ParamSFOData sfoFile; std::string dirPath = GetSaveFilePath(param, GetSaveDir(param, saveDirName)); std::string sfopath = dirPath + "/" + SFO_FILENAME; PSPFileInfo sfoInfo = pspFileSystem.GetFileInfo(sfopath); if(sfoInfo.exists) // Read sfo { std::vector sfoData; if (pspFileSystem.ReadEntireFile(sfopath, sfoData) >= 0) { sfoFile.ReadSFO(sfoData); // save created in PPSSPP and not encrypted has '0' in SAVEDATA_PARAMS u32 tmpDataSize = 0; const u8 *tmpDataOrig = sfoFile.GetValueData("SAVEDATA_PARAMS", &tmpDataSize); if (tmpDataSize == 0 || !tmpDataOrig) { return 0; } switch (tmpDataOrig[0]) { case 0: return 0; case 0x01: return 1; case 0x21: return 3; case 0x41: return 5; default: // Well, it's not zero, so yes. ERROR_LOG_REPORT(SCEUTILITY, "Unexpected SAVEDATA_PARAMS hash flag: %02x", tmpDataOrig[0]); return 1; } } } return 0; } bool SavedataParam::IsInSaveDataList(std::string saveName, int count) { for(int i = 0; i < count; ++i) { if(strcmp(saveDataList[i].saveName.c_str(),saveName.c_str()) == 0) return true; } return false; } bool SavedataParam::IsSaveDirectoryExist(SceUtilitySavedataParam* param) { std::string dirPath = savePath + GetGameName(param) + GetSaveName(param); PSPFileInfo directoryInfo = pspFileSystem.GetFileInfo(dirPath); return directoryInfo.exists; } bool SavedataParam::IsSfoFileExist(SceUtilitySavedataParam* param) { std::string dirPath = savePath + GetGameName(param) + GetSaveName(param); std::string sfoPath = dirPath + "/" + SFO_FILENAME; PSPFileInfo sfoInfo = pspFileSystem.GetFileInfo(sfoPath); return sfoInfo.exists; }