From 5fed277cf27d03e1b5f56415b923b892eeed6bb7 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 28 Oct 2026 20:50:37 +0000 Subject: [PATCH] feat(libSceSaveData): implement sceSaveDataCheckBackupData, sceSaveDataRestoreBackupData and sceSaveDataUmountWithBackup --- core/libs/SceTypes.hpp | 21 ++ .../libs/prx/libSceSaveData.native/Export.cpp | 214 +++++++++++++++--- .../prx/libSceSaveData.native/SaveData.hpp | 1 + core/libs/prx/libSceSaveData/include/Api.hpp | 6 +- core/libs/tests/GuestSaveDataMountStatus.cpp | 101 +++++++++ docs/dev/debt/libSceSaveData.native.md | 5 +- 6 files changed, 317 insertions(+), 31 deletions(-) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 2290a2a3b3..ed9cdf03b7 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1819,6 +1819,27 @@ struct SaveDataBackup { std::uint8_t reserved[32]; }; +struct SaveDataCheckBackupData { + std::int32_t user_id; + std::int32_t pad; + const SceSaveDataTitleId* title_id; + const SceSaveDataDirName* dir_name; + SaveDataParam* param; + SaveDataIcon* icon; + std::uint8_t reserved[32]; +}; + +struct SaveDataRestoreBackupData { + std::int32_t user_id; + std::int32_t pad; + const SceSaveDataTitleId* title_id; + const SceSaveDataDirName* dir_name; + const void* fingerprint; + std::uint32_t unused; + std::uint8_t reserved[32]; + std::int32_t pad2; +}; + struct SaveDataConvert { std::int32_t user_id; std::int32_t pad; diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 2caada7a85..d2d69b5f19 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -38,6 +38,7 @@ static std::string save_root() { // Events the title polls with sceSaveDataGetEventResult; a backup completes at once here. static std::mutex g_state_mutex; static std::deque g_events; +constexpr std::uint32_t SAVE_DATA_EVENT_TYPE_UMOUNT_BACKUP = 1; constexpr std::uint32_t SAVE_DATA_EVENT_TYPE_BACKUP = 2; constexpr std::uint32_t SAVE_DATA_EVENT_TYPE_CONVERT = 5; @@ -328,16 +329,81 @@ static bool isMountedDirectory(const std::string& path) { return false; } +static constexpr char BACKUP_DIR[] = "_sd_backup"; +static constexpr char BACKUP_STAGING_DIR[] = "_sd_backup_tmp"; +static constexpr char BACKUP_OLD_DIR[] = "_sd_backup_old"; + +static std::string backup_entry(const char* root, const std::string& dirName) { + return std::string(root) + "/" + dirName; +} + +static std::string entry_save(const std::string& entry) { + return entry + "/save"; +} + +static bool parse_dir_name(const SceSaveDataDirName* name, std::string* dirName) { + const auto* nameEnd = static_cast(std::memchr(name->data, '\0', sizeof(name->data))); + if (nameEnd == nullptr) { + return false; + } + dirName->assign(name->data, static_cast(nameEnd - name->data)); + return !dirName->empty() && *dirName != "." && *dirName != ".." && dirName->find_first_of("/\\:") == std::string::npos; +} + +static bool running_title(const SceSaveDataTitleId* titleId) { + return titleId == nullptr || std::strncmp(titleId->data, GetAppTitleId_nid_postfix().value, sizeof(titleId->data)) == 0; +} + +static void backupSave(const std::string& dirName) { + const std::string source = save_root() + "/" + dirName; + const std::string entry = backup_entry(BACKUP_DIR, dirName); + const std::string staged = backup_entry(BACKUP_STAGING_DIR, dirName); + const std::string old = backup_entry(BACKUP_OLD_DIR, dirName); + RemoveTree(staged); + RemoveTree(old); + std::filesystem::create_directories(entry_save(staged)); + std::filesystem::copy(source, entry_save(staged), std::filesystem::copy_options::recursive); + if (!store_param(entry_save(staged), load_param(source))) { + throw std::runtime_error("SaveData: cannot write the backup's param file for " + source); + } + if (std::filesystem::exists(blocks_path(source))) { + std::filesystem::copy_file(blocks_path(source), blocks_path(entry_save(staged))); + } + std::filesystem::create_directories(BACKUP_DIR); + const bool replace = std::filesystem::exists(entry); + if (replace) { + std::filesystem::create_directories(BACKUP_OLD_DIR); + std::filesystem::rename(entry, old); + } + std::filesystem::rename(staged, entry); + if (replace) { + RemoveTree(old); + } +} + +static void restoreSave(const std::string& target, const std::string& backup) { + std::vector current; + for (const auto& file : std::filesystem::directory_iterator(target)) { + current.push_back(file.path()); + } + for (const auto& file : current) { + RemoveTree(file); + } + std::filesystem::copy(backup, target, std::filesystem::copy_options::recursive); + std::filesystem::copy_file(param_path(backup), param_path(target), std::filesystem::copy_options::overwrite_existing); + if (std::filesystem::exists(blocks_path(backup))) { + std::filesystem::copy_file(blocks_path(backup), blocks_path(target), std::filesystem::copy_options::overwrite_existing); + } else { + std::filesystem::remove(blocks_path(target)); + } +} + static int deleteSave(const SaveDataDelete* del) { if (del == nullptr || del->dir_name == nullptr) { return SAVE_DATA_ERROR_PARAMETER; } - const auto* nameEnd = static_cast(std::memchr(del->dir_name->data, '\0', sizeof(del->dir_name->data))); - if (nameEnd == nullptr) { - return SAVE_DATA_ERROR_PARAMETER; - } - const std::string dirName(del->dir_name->data, static_cast(nameEnd - del->dir_name->data)); - if (dirName.empty() || dirName == "." || dirName == ".." || dirName.find_first_of("/\\:") != std::string::npos) { + std::string dirName; + if (!parse_dir_name(del->dir_name, &dirName)) { return SAVE_DATA_ERROR_PARAMETER; } const std::string path = save_root() + "/" + dirName; @@ -349,6 +415,7 @@ static int deleteSave(const SaveDataDelete* del) { std::error_code ec; std::filesystem::remove(param_path(path), ec); std::filesystem::remove(blocks_path(path), ec); + RemoveTree(backup_entry(BACKUP_DIR, dirName)); return SAVE_DATA_OK; } @@ -618,12 +685,8 @@ static int mount3(const SaveDataMount3* mount, SaveDataMountResult* mount_result if (!create && !create2 && !open) { throw std::runtime_error("sceSaveDataMount3: unknown mount_mode"); } - const auto* nameEnd = static_cast(std::memchr(mount->dir_name->data, '\0', sizeof(mount->dir_name->data))); - if (nameEnd == nullptr) { - return SAVE_DATA_ERROR_PARAMETER; - } - const std::string dirName(mount->dir_name->data, static_cast(nameEnd - mount->dir_name->data)); - if (dirName.empty() || dirName == "." || dirName == ".." || dirName.find_first_of("/\\:") != std::string::npos) { + std::string dirName; + if (!parse_dir_name(mount->dir_name, &dirName)) { return SAVE_DATA_ERROR_PARAMETER; } const std::string real_path = save_root() + "/" + dirName; @@ -649,6 +712,7 @@ static int mount3(const SaveDataMount3* mount, SaveDataMountResult* mount_result AddPathAlias_nid_no_patch(mountPoint.c_str(), std::filesystem::absolute(real_path).string().c_str()); std::memset(mount_result, 0, sizeof(*mount_result)); g_slots[slot].used = true; + g_slots[slot].user_id = mount->user_id; g_slots[slot].mount_point = mountPoint; g_slots[slot].real_path = real_path; std::memcpy(mount_result->mount_point.data, mountPoint.c_str(), mountPoint.size() + 1); @@ -943,12 +1007,8 @@ static int convert(const SaveDataConvert* param) { return SAVE_DATA_ERROR_PARAMETER; } g_convertProgress = 0.0f; - const auto* nameEnd = static_cast(std::memchr(param->dir_name->data, '\0', sizeof(param->dir_name->data))); - if (nameEnd == nullptr) { - return SAVE_DATA_ERROR_PARAMETER; - } - const std::string dirName(param->dir_name->data, static_cast(nameEnd - param->dir_name->data)); - if (dirName.empty() || dirName == "." || dirName == ".." || dirName.find_first_of("/\\:") != std::string::npos) { + std::string dirName; + if (!parse_dir_name(param->dir_name, &dirName)) { return SAVE_DATA_ERROR_PARAMETER; } if (!std::filesystem::is_directory(save_root() + "/" + dirName)) { @@ -1065,9 +1125,38 @@ int APS5_VABI sceSaveDataChangeInternal(void) { return 0; } -int APS5_VABI sceSaveDataCheckBackupData(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +static int checkBackupData(const SaveDataCheckBackupData* check) { + if (g_initializations == 0) { + return SAVE_DATA_ERROR_NOT_INITIALIZED; + } + if (check == nullptr || check->dir_name == nullptr) { + return SAVE_DATA_ERROR_PARAMETER; + } + std::string dirName; + if (!parse_dir_name(check->dir_name, &dirName)) { + return SAVE_DATA_ERROR_PARAMETER; + } + if (!running_title(check->title_id)) { + return SAVE_DATA_ERROR_NOT_FOUND; + } + std::lock_guard lock(g_slots_mutex); + if (isMountedDirectory(save_root() + "/" + dirName)) { + return SAVE_DATA_ERROR_BUSY; + } + const std::string backup = entry_save(backup_entry(BACKUP_DIR, dirName)); + if (!std::filesystem::is_directory(backup)) { + return SAVE_DATA_ERROR_NOT_FOUND; + } + if (check->param != nullptr && !read_param_file(param_path(backup), check->param)) { + return SAVE_DATA_ERROR_INTERNAL; + } + return SAVE_DATA_OK; +} + +int APS5_VABI sceSaveDataCheckBackupData(const SaveDataCheckBackupData* check) { + const int rc = checkBackupData(check); + SAVEDATA_TRACE("checkBackupData dir=%.*s -> 0x%08x", static_cast(sizeof(SceSaveDataDirName::data)), check != nullptr && check->dir_name != nullptr ? check->dir_name->data : "(null)", static_cast(rc)); + return rc; } int APS5_VABI sceSaveDataCheckBackupDataForCdlg(void) { @@ -1305,9 +1394,44 @@ int APS5_VABI sceSaveDataRegisterEventCallback() { return 0; } -int APS5_VABI sceSaveDataRestoreBackupData(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +static int restoreBackupData(const SaveDataRestoreBackupData* restore) { + if (g_initializations == 0) { + return SAVE_DATA_ERROR_NOT_INITIALIZED; + } + if (restore == nullptr || restore->dir_name == nullptr) { + return SAVE_DATA_ERROR_PARAMETER; + } + std::string dirName; + if (!parse_dir_name(restore->dir_name, &dirName)) { + return SAVE_DATA_ERROR_PARAMETER; + } + if (!running_title(restore->title_id)) { + return SAVE_DATA_ERROR_NOT_FOUND; + } + const std::string target = save_root() + "/" + dirName; + std::lock_guard lock(g_slots_mutex); + if (isMountedDirectory(target)) { + return SAVE_DATA_ERROR_BUSY; + } + const std::string backup = entry_save(backup_entry(BACKUP_DIR, dirName)); + if (!std::filesystem::is_directory(backup)) { + return SAVE_DATA_ERROR_NOT_FOUND; + } + if (!std::filesystem::is_directory(target)) { + return SAVE_DATA_ERROR_INTERNAL; + } + try { + restoreSave(target, backup); + } catch (const std::filesystem::filesystem_error&) { + return SAVE_DATA_ERROR_INTERNAL; + } + return SAVE_DATA_OK; +} + +int APS5_VABI sceSaveDataRestoreBackupData(const SaveDataRestoreBackupData* restore) { + const int rc = restoreBackupData(restore); + SAVEDATA_TRACE("restoreBackupData dir=%.*s -> 0x%08x", static_cast(sizeof(SceSaveDataDirName::data)), restore != nullptr && restore->dir_name != nullptr ? restore->dir_name->data : "(null)", static_cast(rc)); + return rc; } int APS5_VABI sceSaveDataRestoreBackupDataForCdlg(void) { @@ -1355,9 +1479,45 @@ int APS5_VABI sceSaveDataUmountSys(void) { return 0; } -int APS5_VABI sceSaveDataUmountWithBackup(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +static int umountWithBackup(const SaveDataMountPoint* mount_point) { + if (g_initializations == 0) { + return SAVE_DATA_ERROR_NOT_INITIALIZED; + } + if (mount_point == nullptr) { + return SAVE_DATA_ERROR_PARAMETER; + } + SaveDataEvent event{}; + event.type = SAVE_DATA_EVENT_TYPE_UMOUNT_BACKUP; + event.error_code = SAVE_DATA_OK; + { + std::lock_guard lock(g_slots_mutex); + const int slot = find_slot_by_mount_point(mount_point->data); + if (slot == -1) { + return SAVE_DATA_ERROR_NOT_MOUNTED; + } + const AppTitleId title = GetAppTitleId_nid_postfix(); + const MountSlot mounted = g_slots[slot]; + RemovePathAlias_nid_no_patch(mounted.mount_point.c_str()); + g_slots[slot] = MountSlot{}; + const std::string dirName = std::filesystem::path(mounted.real_path).filename().string(); + event.user_id = mounted.user_id; + std::memcpy(event.title_id.data, title.value, strnlen(title.value, sizeof(event.title_id.data) - 1)); + std::memcpy(event.dir_name.data, dirName.data(), std::min(dirName.size(), sizeof(event.dir_name.data) - 1)); + try { + backupSave(dirName); + } catch (const std::exception&) { + event.error_code = SAVE_DATA_ERROR_INTERNAL; + } + } + std::lock_guard lock(g_state_mutex); + g_events.push_back(event); + return SAVE_DATA_OK; +} + +int APS5_VABI sceSaveDataUmountWithBackup(const SaveDataMountPoint* mount_point) { + const int rc = umountWithBackup(mount_point); + SAVEDATA_TRACE("umountWithBackup point=%s -> 0x%08x", mount_point != nullptr ? mount_point->data : "(null)", static_cast(rc)); + return rc; } int APS5_VABI sceSaveDataUnregisterEventCallback() { diff --git a/core/libs/prx/libSceSaveData.native/SaveData.hpp b/core/libs/prx/libSceSaveData.native/SaveData.hpp index 1a8681cb38..aaab18ea96 100644 --- a/core/libs/prx/libSceSaveData.native/SaveData.hpp +++ b/core/libs/prx/libSceSaveData.native/SaveData.hpp @@ -45,6 +45,7 @@ constexpr std::size_t SAVE_DATA_MOUNT_SLOTS = 16; struct MountSlot { bool used = false; + std::int32_t user_id = 0; std::string mount_point; std::string real_path; }; diff --git a/core/libs/prx/libSceSaveData/include/Api.hpp b/core/libs/prx/libSceSaveData/include/Api.hpp index 88067a68e5..b4974c8ce4 100644 --- a/core/libs/prx/libSceSaveData/include/Api.hpp +++ b/core/libs/prx/libSceSaveData/include/Api.hpp @@ -18,7 +18,7 @@ int APS5_VABI sceSaveDataBindPsnAccount(void); int APS5_VABI sceSaveDataBindPsnAccountForSystemBackup(void); int APS5_VABI sceSaveDataChangeDatabase(void); int APS5_VABI sceSaveDataChangeInternal(void); -int APS5_VABI sceSaveDataCheckBackupData(void); +int APS5_VABI sceSaveDataCheckBackupData(const SaveDataCheckBackupData* check); int APS5_VABI sceSaveDataCheckBackupDataForCdlg(void); int APS5_VABI sceSaveDataCheckBackupDataInternal(void); int APS5_VABI sceSaveDataCheckBackupDataLight(void); @@ -66,7 +66,7 @@ int APS5_VABI sceSaveDataMountInternal(void); int APS5_VABI sceSaveDataMountSys(void); int APS5_VABI sceSaveDataRebuildDatabase(void); int APS5_VABI sceSaveDataRegisterEventCallback(); -int APS5_VABI sceSaveDataRestoreBackupData(void); +int APS5_VABI sceSaveDataRestoreBackupData(const SaveDataRestoreBackupData* restore); int APS5_VABI sceSaveDataRestoreBackupDataForCdlg(void); int APS5_VABI sceSaveDataRestoreLoadSaveDataMemory(void); int APS5_VABI sceSaveDataSetAutoUploadSetting(void); @@ -76,7 +76,7 @@ int APS5_VABI sceSaveDataShutdownStart(void); int APS5_VABI sceSaveDataSupportedFakeBrokenStatus(void); int APS5_VABI sceSaveDataSyncCloudList(void); int APS5_VABI sceSaveDataUmountSys(void); -int APS5_VABI sceSaveDataUmountWithBackup(void); +int APS5_VABI sceSaveDataUmountWithBackup(const SaveDataMountPoint* mount_point); int APS5_VABI sceSaveDataUnregisterEventCallback(); int APS5_VABI sceSaveDataUpload(void); diff --git a/core/libs/tests/GuestSaveDataMountStatus.cpp b/core/libs/tests/GuestSaveDataMountStatus.cpp index 3717f20b4a..869f46a516 100644 --- a/core/libs/tests/GuestSaveDataMountStatus.cpp +++ b/core/libs/tests/GuestSaveDataMountStatus.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include #include #include @@ -19,6 +21,7 @@ constexpr std::uint32_t MountReadWrite = 2; constexpr std::uint32_t MountCreate = 4; constexpr std::uint32_t MountCreate2 = 32; constexpr int SaveDataErrorParameter = -2137063424; +constexpr int SaveDataErrorNotInitialized = -2137063423; constexpr int SaveDataErrorNotMounted = -2137063420; constexpr int SaveDataErrorBusy = -2137063421; constexpr int SaveDataErrorNotFound = -2137063416; @@ -66,6 +69,15 @@ void CheckUnmountedCalls() { SaveDataEvent emptyEvent{}; Check(sceSaveDataGetEventResult(nullptr, &emptyEvent) == SaveDataErrorNotFound, "getEventResult on empty queue returns NOT_FOUND"); Check(sceSaveDataGetEventResult(&emptyEvent, nullptr) == SaveDataErrorParameter, "getEventResult null result pointer is PARAMETER"); + SceSaveDataDirName dirName{}; + std::memcpy(dirName.data, "fresh", sizeof("fresh")); + SaveDataCheckBackupData check{}; + check.dir_name = &dirName; + Check(sceSaveDataCheckBackupData(&check) == SaveDataErrorNotInitialized, "checkBackupData before initialize is NOT_INITIALIZED"); + SaveDataRestoreBackupData restore{}; + restore.dir_name = &dirName; + Check(sceSaveDataRestoreBackupData(&restore) == SaveDataErrorNotInitialized, "restoreBackupData before initialize is NOT_INITIALIZED"); + Check(sceSaveDataUmountWithBackup(&point) == SaveDataErrorNotInitialized, "umountWithBackup before initialize is NOT_INITIALIZED"); } void CheckMountInfoAndParam(const SaveDataMountPoint* point) { @@ -236,6 +248,94 @@ void CheckInvalidNamesAreRejected() { MountAndCheck("after", MountReadWrite | MountCreate2, 1u, "CREATE2 after the rejected names"); } +void WriteText(const std::filesystem::path& path, const std::string& text) { + std::ofstream file(path, std::ios::binary | std::ios::trunc); + file << text; + Check(static_cast(file), "writes " + path.string()); +} + +std::string ReadText(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + return std::string(std::istreambuf_iterator(file), std::istreambuf_iterator()); +} + +bool MountSave(SceSaveDataDirName* dirName, std::uint32_t mode, SaveDataMountResult* result) { + SaveDataMount3 mount{}; + mount.user_id = 7; + mount.dir_name = dirName; + mount.mount_mode = mode; + return sceSaveDataMount3(&mount, result) == 0; +} + +void CheckBackups() { + std::filesystem::create_directories("app0/sce_sys"); + WriteText("app0/sce_sys/param.json", "{\"titleId\":\"PPSA00000\",\"localizedParameters\":{\"en-US\":{\"titleName\":\"Example\"}}}"); + SceSaveDataDirName kept{}; + std::memcpy(kept.data, "kept", sizeof("kept")); + SceSaveDataDirName fresh{}; + std::memcpy(fresh.data, "fresh", sizeof("fresh")); + SaveDataCheckBackupData check{}; + SaveDataRestoreBackupData restore{}; + Check(sceSaveDataCheckBackupData(nullptr) == SaveDataErrorParameter, "checkBackupData null block is PARAMETER"); + Check(sceSaveDataCheckBackupData(&check) == SaveDataErrorParameter, "checkBackupData null dir_name is PARAMETER"); + Check(sceSaveDataRestoreBackupData(nullptr) == SaveDataErrorParameter, "restoreBackupData null block is PARAMETER"); + Check(sceSaveDataRestoreBackupData(&restore) == SaveDataErrorParameter, "restoreBackupData null dir_name is PARAMETER"); + Check(sceSaveDataUmountWithBackup(nullptr) == SaveDataErrorParameter, "umountWithBackup null point is PARAMETER"); + check.dir_name = &fresh; + restore.dir_name = &fresh; + Check(sceSaveDataCheckBackupData(&check) == SaveDataErrorNotFound, "checkBackupData without a backup is NOT_FOUND"); + Check(sceSaveDataRestoreBackupData(&restore) == SaveDataErrorNotFound, "restoreBackupData without a backup is NOT_FOUND"); + + SaveDataMountResult mounted{}; + Check(MountSave(&kept, MountReadWrite | MountCreate2, &mounted), "the backed-up save mounts"); + const auto data = std::filesystem::path("_sd") / "kept" / "data.bin"; + WriteText(data, "first"); + const char backedUpTitle[] = "Backed Up"; + Check(sceSaveDataSetParam(&mounted.mount_point, 1, backedUpTitle, sizeof(backedUpTitle)) == 0, "the backed-up save takes a title"); + check.dir_name = &kept; + restore.dir_name = &kept; + Check(sceSaveDataCheckBackupData(&check) == SaveDataErrorBusy, "checkBackupData of a mounted save is BUSY"); + Check(sceSaveDataRestoreBackupData(&restore) == SaveDataErrorBusy, "restoreBackupData of a mounted save is BUSY"); + Check(sceSaveDataUmountWithBackup(&mounted.mount_point) == 0, "umountWithBackup succeeds"); + SaveDataMountInfo info{}; + Check(sceSaveDataGetMountInfo(&mounted.mount_point, &info) == SaveDataErrorNotMounted, "umountWithBackup unmounts the save"); + + SaveDataEvent event{}; + Check(sceSaveDataGetEventResult(nullptr, &event) == 0, "umountWithBackup posts an event"); + Check(event.type == 1, "the event is UMOUNT_BACKUP, got type " + std::to_string(event.type)); + Check(event.error_code == 0, "the backup succeeded, error " + std::to_string(event.error_code)); + Check(event.user_id == 7, "the event names the mounting user"); + Check(std::strncmp(event.title_id.data, "PPSA00000", sizeof(event.title_id.data)) == 0, "the event names the running title"); + Check(std::strncmp(event.dir_name.data, "kept", sizeof(event.dir_name.data)) == 0, "the event names the save"); + + SaveDataParam param{}; + check.param = ¶m; + Check(sceSaveDataCheckBackupData(&check) == 0, "checkBackupData finds the backup"); + Check(std::strcmp(param.title, backedUpTitle) == 0, "checkBackupData fills param from the backup"); + SceSaveDataTitleId running{}; + std::memcpy(running.data, "PPSA00000", sizeof("PPSA00000")); + check.title_id = &running; + Check(sceSaveDataCheckBackupData(&check) == 0, "checkBackupData with the running title's id finds the backup"); + + WriteText(data, "second"); + WriteText(std::filesystem::path("_sd") / "kept" / "added.bin", "added"); + Check(MountSave(&kept, MountReadWrite, &mounted), "the save mounts again after the backup"); + const char changedTitle[] = "Changed"; + Check(sceSaveDataSetParam(&mounted.mount_point, 1, changedTitle, sizeof(changedTitle)) == 0, "the title changes after the backup"); + Check(sceSaveDataUmount2(0, &mounted.mount_point) == 0, "the changed save unmounts"); + Check(sceSaveDataRestoreBackupData(&restore) == 0, "restoreBackupData succeeds"); + Check(ReadText(data) == "first", "restoreBackupData puts back a file changed after the backup"); + Check(!std::filesystem::exists(std::filesystem::path("_sd") / "kept" / "added.bin"), "restoreBackupData removes a file added after the backup"); + char title[128]{}; + Check(MountSave(&kept, MountReadWrite, &mounted), "the restored save mounts"); + Check(sceSaveDataGetParam(&mounted.mount_point, 1, title, sizeof(title), nullptr) == 0 && std::strcmp(title, backedUpTitle) == 0, "restoreBackupData puts back the backed-up title"); + Check(sceSaveDataUmount2(0, &mounted.mount_point) == 0, "the restored save unmounts"); + SaveDataDelete del{}; + del.dir_name = &kept; + Check(sceSaveDataDelete(&del) == 0, "the backed-up save is deleted"); + Check(sceSaveDataCheckBackupData(&check) == SaveDataErrorNotFound, "deleting a save deletes its backup"); +} + } int main() { @@ -253,6 +353,7 @@ int main() { CheckFailedMountKeepsResult(); CheckEquivalentMounts(); CheckInvalidNamesAreRejected(); + CheckBackups(); std::filesystem::current_path(previous); std::error_code error; std::filesystem::remove_all(root, error); diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index d716665d15..0c4980d3ec 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -16,7 +16,7 @@ - [sceSaveDataDirNameSearch, sceSaveDataGetMountInfo, sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the 64 KiB block of [SharpEmu](https://github.com/sharpemu/sharpemu/blob/c6d304cc3328bad62cb671327ace181110cf2ddb/src/SharpEmu.Libs/SaveData/SaveDataExports.cs#L82-L84) and the 48 to 16384-block range of SharpEmu and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L30-L31) are not measured on a console, and file system overhead is not counted. What the console does with a `blocks` outside that range is unknown: KytyPS5 rejects the creating mount and SharpEmu clamps it; here a larger one is kept as 16384 and a smaller non-zero one as given. A save's size is the `blocks` of the mount that created it, kept in `.blocks`, else that of its next mount that names one, else 16384; its free blocks are that size less its files, each rounded up to 64 KiB blocks - [sceSaveDataInitialize3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a repeated initialize succeeds: PPSA12544's executable and its Unity SaveData plugin both initialize, and the plugin fails on any error. Initializations are counted and `sceSaveDataTerminate` ends the session at the last one; how the console pairs them is unknown. In PPSA12544 the executable calls it once (0x1721d8f) with 0 as its argument, only while a flag byte of its object is 0, and does not import `sceSaveDataTerminate`; the plugin calls it once (0x1fd2) with 0, skips the call while a flag byte of its own is set, and calls `sceSaveDataTerminate` from two functions (0x2059, 0x20d9) that clear that byte. - [sceSaveDataCheckIpmiIfSize, sceSaveDataDebug, sceSaveDataDebugCheckBackupData, sceSaveDataDebugCleanMount, sceSaveDataDebugCompiledSdkVersion, sceSaveDataDebugCreateSaveDataRoot, sceSaveDataDebugProspero, sceSaveDataDebugRemoveSaveDataRoot and sceSaveDataDebugTarget](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSaveData.native.c). The seven no-argument prototypes follow the PS4 stubs in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/save_data/savedata.cpp#L653-L725), not measured on a console; no credible prototypes were found for the other two. All declarations have no parameters solely to resolve known imports and make no ABI claim. -- The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`sceSaveDataAbort`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataBindPsnAccount`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataBindPsnAccountForSystemBackup`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataChangeDatabase`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataChangeInternal`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckBackupData`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckBackupDataForCdlg`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckBackupDataInternal`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckBackupDataLight`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckCloudData`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckSaveDataBroken`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckSaveDataVersion`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckSaveDataVersionLatest`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataClearProgress`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCreateUploadData`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDebugGetThreadId`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDeleteAllUser`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDeleteBackupData`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDeleteCloudData`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDeleteProspero`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDeleteTitle`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDeleteUser`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDirNameSearchInternal`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDownload`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetAllSize`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetAppLaunchedUser`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetAutoUploadConditions`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetAutoUploadRequestInfo`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetAutoUploadSetting`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetBoundPsnAccountCount`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetClientThreadPriority`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetCloudQuotaInfo`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetEventInfo`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetMountedSaveDataCount`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetProgress`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetSaveDataCount`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetSaveDataRootDir`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetSaveDataRootPath`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetSaveDataRootUsbPath`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetSavePoint`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetUpdatedDataCount`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataInitialize2`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataInitializeForCdlg`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataIsDeletingUsbDb`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataIsMounted`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataManualDelete`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataManualDownload`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataManualUpload`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataMount5`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataMountInternal`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataMountSys`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataRebuildDatabase`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataRegisterEventCallback`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataRestoreBackupData`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataRestoreBackupDataForCdlg`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataRestoreLoadSaveDataMemory`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataSetAutoUploadSetting`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataSetEventInfo`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataSetSaveDataLibraryUser`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataShutdownStart`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataSupportedFakeBrokenStatus`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataSyncCloudList`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataUmountSys`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataUmountWithBackup`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataUnregisterEventCallback`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataUpload`](../../../core/libs/prx/libSceSaveData.native/Export.cpp). +- The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`sceSaveDataAbort`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataBindPsnAccount`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataBindPsnAccountForSystemBackup`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataChangeDatabase`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataChangeInternal`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckBackupDataForCdlg`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckBackupDataInternal`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckBackupDataLight`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckCloudData`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckSaveDataBroken`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckSaveDataVersion`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckSaveDataVersionLatest`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataClearProgress`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCreateUploadData`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDebugGetThreadId`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDeleteAllUser`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDeleteBackupData`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDeleteCloudData`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDeleteProspero`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDeleteTitle`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDeleteUser`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDirNameSearchInternal`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDownload`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetAllSize`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetAppLaunchedUser`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetAutoUploadConditions`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetAutoUploadRequestInfo`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetAutoUploadSetting`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetBoundPsnAccountCount`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetClientThreadPriority`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetCloudQuotaInfo`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetEventInfo`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetMountedSaveDataCount`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetProgress`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetSaveDataCount`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetSaveDataRootDir`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetSaveDataRootPath`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetSaveDataRootUsbPath`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetSavePoint`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetUpdatedDataCount`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataInitialize2`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataInitializeForCdlg`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataIsDeletingUsbDb`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataIsMounted`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataManualDelete`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataManualDownload`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataManualUpload`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataMount5`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataMountInternal`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataMountSys`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataRebuildDatabase`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataRegisterEventCallback`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataRestoreBackupDataForCdlg`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataRestoreLoadSaveDataMemory`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataSetAutoUploadSetting`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataSetEventInfo`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataSetSaveDataLibraryUser`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataShutdownStart`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataSupportedFakeBrokenStatus`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataSyncCloudList`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataUmountSys`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataUnregisterEventCallback`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataUpload`](../../../core/libs/prx/libSceSaveData.native/Export.cpp). ### Functional @@ -24,6 +24,9 @@ - [sceSaveDataDirNameSearch2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) runs the same search as `sceSaveDataDirNameSearch` with the same condition and result structures, as [SharpEmu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/SaveData/SaveDataExports.cs) does, its only source; what distinguishes it on the console is unknown. One title (PPSA35669, in a plugin) disassembled for this entry has one call site (0x2eda71); it clears 56 bytes for the result structure first (the size of the structure used here), puts a value returned by a call with the argument 0x8000 at offset 8 and 0x400 at offset 0x10, and afterwards branches on a negative result and reads the 32-bit field at offset 0, skipping its loop when it is 0. That agrees with the result structure of `sceSaveDataDirNameSearch`. The condition structure was not read. - [sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a failed mount (BUSY, EXISTS, NOT_FOUND, MOUNT_FULL) leaves the result as it was, as shadPS4 does; not checked on hardware. Animal Well (PPSA22520) depends on it: it checks for its save with a mount it never unmounts, then saves with a second mount of the same directory, ignores its BUSY and keeps using the first mount point for `sceSaveDataPrepare`, its file and `sceSaveDataUmount2`. In PPSA05347 the function that calls it at 0x235fbb5 zeroes its 0x40-byte result before the call and, for a negative result other than 0x809f0008 and 0x809f000f (BUSY included), logs the code and returns it without recording the mount; no abort follows. - [sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - an unterminated or empty directory name, `.`, `..`, or a name containing `/`, `\` or `:` returns `SAVE_DATA_ERROR_PARAMETER`, which the [SDK 12.000 reference](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-mount3.md#L196-L204) lists for an invalid parameter, and leaves the result as it was, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L652-L658) does. The [SDK](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-dir-name.md#L32-L36) lets an application use only letters, digits, `-`, `.` and `@` in a name, and KytyPS5 also accepts `_` ([`valid_path_component`](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L276-L287)); which other names the console's mount rejects is not checked on hardware +- [sceSaveDataCheckBackupData, sceSaveDataRestoreBackupData, sceSaveDataUmountWithBackup](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - not measured on a PS5: the block layouts, error checks and behaviour follow shadPS4's PS4 binding ([check](https://github.com/shadps4-emu/shadPS4/blob/ce74386283f559f2279a7c31416bcdc9359be0b3/src/core/libraries/save_data/savedata.cpp#L615-L664), [restore](https://github.com/shadps4-emu/shadPS4/blob/ce74386283f559f2279a7c31416bcdc9359be0b3/src/core/libraries/save_data/savedata.cpp#L1360-L1398), [unmount with backup](https://github.com/shadps4-emu/shadPS4/blob/ce74386283f559f2279a7c31416bcdc9359be0b3/src/core/libraries/save_data/savedata.cpp#L1758-L1761)), and the tests check that binding, not the console. `sceSaveDataBackup` still copies nothing, so the check and the restore see only backups that `sceSaveDataUmountWithBackup` made. +- [sceSaveDataUmountWithBackup](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - an unknown mount point returns `SAVE_DATA_ERROR_NOT_MOUNTED`, as `sceSaveDataUmount2` does, where [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/ce74386283f559f2279a7c31416bcdc9359be0b3/src/core/libraries/save_data/savedata.cpp#L512-L543) returns `SAVE_DATA_ERROR_NOT_FOUND`. The backup is copied before the call returns, so `SAVE_DATA_ERROR_BACKUP_BUSY` never occurs, and the `UMOUNT_BACKUP` event is queued at once, as [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/src/ps4_libscesavedata.pas#L444-L462) does; a failed copy sets its error to `SAVE_DATA_ERROR_INTERNAL`, as shadPS4's [backup thread](https://github.com/shadps4-emu/shadPS4/blob/ce74386283f559f2279a7c31416bcdc9359be0b3/src/core/libraries/save_data/save_backup.cpp#L167-L173) does. +- [sceSaveDataCheckBackupData, sceSaveDataRestoreBackupData](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - saves keep no icons, so the check never fills `icon`, as shadPS4 does without `icon0.png`, and another title's `titleId` returns `SAVE_DATA_ERROR_NOT_FOUND` because only the running title's saves are kept. Backups live in `_sd_backup/`, outside the save, and `sceSaveDataDelete` removes one with its save, as shadPS4's [backup](https://github.com/shadps4-emu/shadPS4/blob/ce74386283f559f2279a7c31416bcdc9359be0b3/src/core/libraries/save_data/save_backup.cpp#L93-L134) inside the save directory goes with it; whether the console keeps a deleted save's backup is unknown. - [sceSaveDataBackup, sceSaveDataConvert, sceSaveDataDelete, sceSaveDataDirNameSearch, sceSaveDataDirNameSearchPs4, sceSaveDataGetEventResult, sceSaveDataGetMountInfo, sceSaveDataGetParam, sceSaveDataLoadIcon, sceSaveDataMount, sceSaveDataMount3, sceSaveDataSaveIcon, sceSaveDataSetParam, sceSaveDataUmount2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - null pointer arguments return `SAVE_DATA_ERROR_PARAMETER` instead of throwing C++ exceptions, an implementation assumption matching guest error expectations not checked on hardware; unmounted targets return `SAVE_DATA_ERROR_NOT_MOUNTED`, and parameter type or buffer size violations return `SAVE_DATA_ERROR_PARAMETER` after slot lookup - [sceSaveDataSyncSaveDataMemory](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - validates library initialization and parameters; since in-memory saves flush synchronously on write, sync confirms existence and readiness of the memory blob