mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-11-05 13:20:11 +00:00
backupSave moved the previous backup to _sd_backup_old and then renamed the new copy into place; when that rename failed, the save had no backup until the next one. A failed rename now moves the previous backup back into _sd_backup and removes the staged copy. Every staged file is flushed before the swap (FlushFileBuffers on Windows, fsync on POSIX, as libc does), and on POSIX the directories on both sides of each rename are flushed too, all before the previous backup is removed.
544 lines
30 KiB
C++
544 lines
30 KiB
C++
#include "prx/libSceSaveData/include/Api.hpp"
|
|
#include "prx/libSceSaveData.native/include/Api.hpp"
|
|
#include "prx/libc/include/general/VabiMacros.hpp"
|
|
#include "SceTypes.hpp"
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iterator>
|
|
#include <map>
|
|
#include <random>
|
|
#include <set>
|
|
#include <string>
|
|
#include <system_error>
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#endif
|
|
|
|
namespace {
|
|
|
|
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;
|
|
constexpr int SaveDataErrorInternal = -2137063413;
|
|
|
|
int failures = 0;
|
|
|
|
void Check(bool condition, const std::string& what) {
|
|
if (condition) return;
|
|
std::fprintf(stderr, "savedata mount status check failed: %s\n", what.c_str());
|
|
++failures;
|
|
}
|
|
|
|
void CheckUnmountedCalls() {
|
|
SaveDataMountPoint point{};
|
|
std::memcpy(point.data, "test", sizeof("test"));
|
|
SaveDataMountInfo info{};
|
|
SaveDataIcon icon{};
|
|
char buf[128]{};
|
|
std::size_t gotSize = 0;
|
|
SaveDataMount3 nullMount{};
|
|
SaveDataMountResult mountResult{};
|
|
Check(sceSaveDataMount3(nullptr, &mountResult) == SaveDataErrorParameter, "mount3 null mount is PARAMETER");
|
|
Check(sceSaveDataMount3(&nullMount, nullptr) == SaveDataErrorParameter, "mount3 null result is PARAMETER");
|
|
Check(sceSaveDataMount3(&nullMount, &mountResult) == SaveDataErrorParameter, "mount3 null dir_name is PARAMETER");
|
|
Check(sceSaveDataUmount2(0, nullptr) == SaveDataErrorParameter, "umount2 null point is PARAMETER");
|
|
Check(sceSaveDataUmount2(0, &point) == SaveDataErrorNotMounted, "umount2 unmounted point is NOT_MOUNTED");
|
|
Check(sceSaveDataLoadIcon(nullptr, &icon) == SaveDataErrorParameter, "loadIcon null point is PARAMETER");
|
|
Check(sceSaveDataLoadIcon(&point, &icon) == SaveDataErrorNotMounted, "loadIcon unmounted point is NOT_MOUNTED");
|
|
Check(sceSaveDataSaveIcon(nullptr, &icon) == SaveDataErrorParameter, "saveIcon null point is PARAMETER");
|
|
Check(sceSaveDataSaveIcon(&point, &icon) == SaveDataErrorNotMounted, "saveIcon unmounted point is NOT_MOUNTED");
|
|
Check(sceSaveDataSaveIconByPath(&point, "test.png") == 0, "saveIconByPath succeeds");
|
|
Check(sceSaveDataGetMountInfo(nullptr, &info) == SaveDataErrorParameter, "getMountInfo null point is PARAMETER");
|
|
Check(sceSaveDataGetMountInfo(&point, nullptr) == SaveDataErrorParameter, "getMountInfo null info is PARAMETER");
|
|
Check(sceSaveDataGetMountInfo(&point, &info) == SaveDataErrorNotMounted, "getMountInfo unmounted point is NOT_MOUNTED");
|
|
Check(sceSaveDataGetParam(nullptr, 1, buf, sizeof(buf), &gotSize) == SaveDataErrorParameter, "getParam null point is PARAMETER");
|
|
Check(sceSaveDataGetParam(&point, 1, nullptr, sizeof(buf), &gotSize) == SaveDataErrorParameter, "getParam null buf is PARAMETER");
|
|
Check(sceSaveDataGetParam(&point, 1, buf, sizeof(buf), &gotSize) == SaveDataErrorNotMounted, "getParam unmounted point is NOT_MOUNTED");
|
|
Check(sceSaveDataSetParam(nullptr, 1, buf, sizeof(buf)) == SaveDataErrorParameter, "setParam null point is PARAMETER");
|
|
Check(sceSaveDataSetParam(&point, 1, nullptr, sizeof(buf)) == SaveDataErrorParameter, "setParam null buf is PARAMETER");
|
|
Check(sceSaveDataSetParam(&point, 1, buf, sizeof(buf)) == SaveDataErrorNotMounted, "setParam unmounted point is NOT_MOUNTED");
|
|
Check(sceSaveDataBackup(nullptr) == SaveDataErrorParameter, "backup null backup is PARAMETER");
|
|
SaveDataBackup nullDirBackup{};
|
|
Check(sceSaveDataBackup(&nullDirBackup) == SaveDataErrorParameter, "backup null dir_name is PARAMETER");
|
|
Check(sceSaveDataGetEventResult(nullptr, nullptr) == SaveDataErrorParameter, "getEventResult null event is PARAMETER");
|
|
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) {
|
|
SaveDataMountInfo info{};
|
|
SaveDataIcon icon{};
|
|
char buf[128]{};
|
|
char smallBuf[64]{};
|
|
std::size_t gotSize = 0;
|
|
SaveDataMountPoint unmounted{};
|
|
std::memcpy(unmounted.data, "unmounted", sizeof("unmounted"));
|
|
const char newTitle[] = "Mounted Title";
|
|
char readTitle[128]{};
|
|
Check(sceSaveDataUmount2(0, nullptr) == SaveDataErrorParameter, "umount2 null point is PARAMETER");
|
|
Check(sceSaveDataLoadIcon(nullptr, &icon) == SaveDataErrorParameter, "loadIcon null point is PARAMETER");
|
|
Check(sceSaveDataLoadIcon(point, &icon) == 0, "loadIcon on mounted save succeeds");
|
|
Check(sceSaveDataSaveIcon(nullptr, &icon) == SaveDataErrorParameter, "saveIcon null point is PARAMETER");
|
|
Check(sceSaveDataSaveIcon(point, &icon) == 0, "saveIcon on mounted save succeeds");
|
|
Check(sceSaveDataSaveIconByPath(point, "icon.png") == 0, "saveIconByPath on mounted save succeeds");
|
|
Check(sceSaveDataGetMountInfo(nullptr, &info) == SaveDataErrorParameter, "getMountInfo null point is PARAMETER");
|
|
Check(sceSaveDataGetMountInfo(point, nullptr) == SaveDataErrorParameter, "getMountInfo null info is PARAMETER");
|
|
Check(sceSaveDataGetMountInfo(&unmounted, &info) == SaveDataErrorNotMounted, "getMountInfo unmounted point is NOT_MOUNTED");
|
|
Check(sceSaveDataGetMountInfo(point, &info) == 0, "getMountInfo on mounted save succeeds");
|
|
Check(sceSaveDataGetParam(nullptr, 1, buf, sizeof(buf), &gotSize) == SaveDataErrorParameter, "getParam null point is PARAMETER");
|
|
Check(sceSaveDataGetParam(point, 1, nullptr, sizeof(buf), &gotSize) == SaveDataErrorParameter, "getParam null buf is PARAMETER");
|
|
Check(sceSaveDataGetParam(point, 1, smallBuf, sizeof(smallBuf), &gotSize) == SaveDataErrorParameter, "getParam small buf is PARAMETER");
|
|
Check(sceSaveDataGetParam(&unmounted, 1, buf, sizeof(buf), &gotSize) == SaveDataErrorNotMounted, "getParam unmounted point is NOT_MOUNTED");
|
|
Check(sceSaveDataGetParam(point, 1, buf, sizeof(buf), &gotSize) == 0, "getParam on mounted save succeeds");
|
|
Check(gotSize == sizeof(buf), "getParam reports expected size");
|
|
Check(sceSaveDataGetParam(point, 1, buf, sizeof(buf), nullptr) == 0, "getParam with null got_size on mounted save succeeds");
|
|
Check(sceSaveDataSetParam(nullptr, 1, newTitle, sizeof(newTitle)) == SaveDataErrorParameter, "setParam null point is PARAMETER");
|
|
Check(sceSaveDataSetParam(point, 1, nullptr, sizeof(newTitle)) == SaveDataErrorParameter, "setParam null buf is PARAMETER");
|
|
Check(sceSaveDataSetParam(point, 999, newTitle, sizeof(newTitle)) == SaveDataErrorParameter, "setParam invalid type is PARAMETER");
|
|
Check(sceSaveDataSetParam(point, 1, newTitle, 0) == SaveDataErrorParameter, "setParam zero size is PARAMETER");
|
|
Check(sceSaveDataSetParam(point, 4, smallBuf, 1) == SaveDataErrorParameter, "setParam short user param is PARAMETER");
|
|
Check(sceSaveDataSetParam(&unmounted, 1, newTitle, sizeof(newTitle)) == SaveDataErrorNotMounted, "setParam unmounted point is NOT_MOUNTED");
|
|
Check(sceSaveDataSetParam(point, 1, newTitle, sizeof(newTitle)) == 0, "setParam on mounted save succeeds");
|
|
Check(sceSaveDataGetParam(point, 1, readTitle, sizeof(readTitle), &gotSize) == 0, "getParam reads back set title");
|
|
Check(std::strcmp(readTitle, newTitle) == 0, "readTitle matches newTitle");
|
|
}
|
|
|
|
void MountAndCheck(const char* name, std::uint32_t mode, std::uint32_t expectedStatus, const std::string& what) {
|
|
SceSaveDataDirName dirName{};
|
|
std::memcpy(dirName.data, name, std::strlen(name) + 1);
|
|
SaveDataMount3 mount{};
|
|
mount.dir_name = &dirName;
|
|
mount.mount_mode = mode;
|
|
SaveDataMountResult result{};
|
|
const int code = sceSaveDataMount3(&mount, &result);
|
|
Check(code == 0, what + ": mounts");
|
|
if (code != 0) return;
|
|
Check(result.mount_status == expectedStatus, what + ": status is " + std::to_string(result.mount_status) + ", expected " + std::to_string(expectedStatus));
|
|
CheckMountInfoAndParam(&result.mount_point);
|
|
Check(sceSaveDataUmount2(0, &result.mount_point) == 0, what + ": unmounts");
|
|
}
|
|
|
|
void CheckFailedMountKeepsResult() {
|
|
SceSaveDataDirName held{};
|
|
std::memcpy(held.data, "held", sizeof("held"));
|
|
SaveDataMount3 mount{};
|
|
mount.dir_name = &held;
|
|
mount.mount_mode = MountReadWrite | MountCreate2;
|
|
SaveDataMountResult result{};
|
|
Check(sceSaveDataMount3(&mount, &result) == 0, "first mount of a save");
|
|
const SaveDataMountResult mounted = result;
|
|
Check(sceSaveDataMount3(&mount, &result) == SaveDataErrorBusy, "second mount of a mounted save is BUSY");
|
|
Check(std::memcmp(&result, &mounted, sizeof(result)) == 0, "a BUSY mount leaves the result as it was");
|
|
SceSaveDataDirName absent{};
|
|
std::memcpy(absent.data, "absent", sizeof("absent"));
|
|
mount.dir_name = &absent;
|
|
mount.mount_mode = MountReadWrite;
|
|
Check(sceSaveDataMount3(&mount, &result) == SaveDataErrorNotFound, "open of a missing save is NOT_FOUND");
|
|
Check(std::memcmp(&result, &mounted, sizeof(result)) == 0, "a NOT_FOUND mount leaves the result as it was");
|
|
MountAndCheck("new-create", MountReadWrite | MountCreate, 1u, "CREATE while another save is mounted");
|
|
MountAndCheck("new-create2", MountReadWrite | MountCreate2, 1u, "CREATE2 while another save is mounted");
|
|
Check(std::filesystem::remove(std::filesystem::path("_sd") / "held"), "the mounted backing directory is removed");
|
|
mount.dir_name = &held;
|
|
Check(sceSaveDataMount3(&mount, &result) == SaveDataErrorBusy, "the same name remains BUSY after its backing directory disappears");
|
|
Check(std::memcmp(&result, &mounted, sizeof(result)) == 0, "a missing backing directory leaves the BUSY result unchanged");
|
|
MountAndCheck("made", MountReadWrite, 0u, "open of another save while a mounted backing directory is missing");
|
|
Check(sceSaveDataUmount2(0, &result.mount_point) == 0, "the kept mount point unmounts the save");
|
|
}
|
|
|
|
void CheckEquivalentMounts() {
|
|
SceSaveDataDirName original{};
|
|
std::memcpy(original.data, "identity", sizeof("identity"));
|
|
SaveDataMount3 mount{};
|
|
mount.dir_name = &original;
|
|
mount.mount_mode = MountReadWrite | MountCreate2;
|
|
SaveDataMountResult first{};
|
|
Check(sceSaveDataMount3(&mount, &first) == 0, "original save mounts");
|
|
|
|
const auto originalPath = std::filesystem::path("_sd") / "identity";
|
|
std::error_code error;
|
|
const auto casePath = std::filesystem::path("_sd") / "IDENTITY";
|
|
const bool caseAlias = std::filesystem::exists(casePath) && std::filesystem::equivalent(originalPath, casePath, error);
|
|
Check(!error || error == std::errc::no_such_file_or_directory, "case-alias identity check succeeds");
|
|
#ifdef _WIN32
|
|
error.clear();
|
|
if (!CreateSymbolicLinkW((std::filesystem::path("_sd") / "linked").c_str(), L"identity",
|
|
SYMBOLIC_LINK_FLAG_DIRECTORY | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE)) {
|
|
error = std::error_code(GetLastError(), std::system_category());
|
|
}
|
|
#else
|
|
std::filesystem::create_directory_symlink("identity", std::filesystem::path("_sd") / "linked", error);
|
|
#endif
|
|
const bool symlinkAlias = !error;
|
|
Check(caseAlias || symlinkAlias, "a host alias is available to check mount identity");
|
|
|
|
const auto checkAlias = [&](const char* name) {
|
|
SceSaveDataDirName alias{};
|
|
std::memcpy(alias.data, name, std::strlen(name) + 1);
|
|
mount.dir_name = &alias;
|
|
mount.mount_mode = MountReadWrite;
|
|
SaveDataMountResult result;
|
|
std::memset(&result, 0xCD, sizeof(result));
|
|
const SaveDataMountResult before = result;
|
|
Check(sceSaveDataMount3(&mount, &result) == SaveDataErrorBusy, std::string(name) + " cannot mount the same directory twice");
|
|
Check(std::memcmp(&result, &before, sizeof(result)) == 0, std::string(name) + " leaves the result unchanged");
|
|
};
|
|
if (caseAlias) checkAlias("IDENTITY");
|
|
if (symlinkAlias) checkAlias("linked");
|
|
|
|
Check(sceSaveDataUmount2(0, &first.mount_point) == 0, "original save unmounts");
|
|
if (caseAlias) MountAndCheck("IDENTITY", MountReadWrite, 0u, "case alias after unmount");
|
|
if (symlinkAlias) {
|
|
MountAndCheck("linked", MountReadWrite, 0u, "symlink alias after unmount");
|
|
#ifdef _WIN32
|
|
error.clear();
|
|
if (!RemoveDirectoryW((std::filesystem::path("_sd") / "linked").c_str())) error = std::error_code(GetLastError(), std::system_category());
|
|
#else
|
|
std::filesystem::remove(std::filesystem::path("_sd") / "linked", error);
|
|
#endif
|
|
Check(!error, "symlink alias is removed");
|
|
}
|
|
}
|
|
|
|
std::set<std::filesystem::path> Tree() {
|
|
std::set<std::filesystem::path> paths;
|
|
for (const auto& entry : std::filesystem::recursive_directory_iterator(std::filesystem::current_path())) paths.insert(entry.path());
|
|
return paths;
|
|
}
|
|
|
|
void CheckInvalidNameIsRejected(const char* data, std::size_t size, const std::string& label) {
|
|
for (const std::uint32_t mode : {MountReadWrite | MountCreate2, MountReadWrite | MountCreate, MountReadWrite}) {
|
|
SceSaveDataDirName dirName{};
|
|
std::memcpy(dirName.data, data, size);
|
|
SaveDataMount3 mount{};
|
|
mount.dir_name = &dirName;
|
|
mount.mount_mode = mode;
|
|
SaveDataMountResult result;
|
|
std::memset(&result, 0xCD, sizeof(result));
|
|
const SaveDataMountResult before = result;
|
|
const std::string what = label + " with mount mode " + std::to_string(mode);
|
|
Check(sceSaveDataMount3(&mount, &result) == SaveDataErrorParameter, what + " is a parameter error");
|
|
Check(std::memcmp(&result, &before, sizeof(result)) == 0, what + " leaves the result as it was");
|
|
}
|
|
}
|
|
|
|
void CheckInvalidNamesAreRejected() {
|
|
const auto before = Tree();
|
|
for (const char* invalid : {"", ".", "..", "a/b", "a\\b", "c:d"}) {
|
|
CheckInvalidNameIsRejected(invalid, std::strlen(invalid) + 1, std::string("\"") + invalid + "\"");
|
|
}
|
|
char unterminated[sizeof(SceSaveDataDirName::data)];
|
|
std::memset(unterminated, 'a', sizeof(unterminated));
|
|
CheckInvalidNameIsRejected(unterminated, sizeof(unterminated), "an unterminated name");
|
|
Check(Tree() == before, "rejected names create nothing");
|
|
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<bool>(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<char>(file), std::istreambuf_iterator<char>());
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
std::map<std::string, std::string> Snapshot(const std::filesystem::path& save) {
|
|
std::map<std::string, std::string> files;
|
|
for (const char* side : {".param", ".blocks"}) {
|
|
std::filesystem::path path = save;
|
|
path += side;
|
|
if (std::filesystem::exists(path)) files[side] = ReadText(path);
|
|
}
|
|
if (!std::filesystem::is_directory(save)) return files;
|
|
files["/"] = "";
|
|
for (const auto& entry : std::filesystem::recursive_directory_iterator(save)) {
|
|
const std::string relative = std::filesystem::relative(entry.path(), save).generic_string();
|
|
if (entry.is_directory()) {
|
|
files[relative + "/"] = "";
|
|
} else {
|
|
files[relative] = ReadText(entry.path());
|
|
}
|
|
}
|
|
return files;
|
|
}
|
|
|
|
int BackUp(SceSaveDataDirName* dirName, const std::string& data, const char* title) {
|
|
const std::string name = dirName->data;
|
|
SaveDataMountResult mounted{};
|
|
if (!MountSave(dirName, MountReadWrite | MountCreate2, &mounted)) {
|
|
Check(false, name + " mounts to be backed up");
|
|
return SaveDataErrorNotMounted;
|
|
}
|
|
WriteText(std::filesystem::path("_sd") / name / "data.bin", data);
|
|
Check(sceSaveDataSetParam(&mounted.mount_point, 1, title, std::strlen(title) + 1) == 0, name + " takes the title " + title);
|
|
Check(sceSaveDataUmountWithBackup(&mounted.mount_point) == 0, name + " unmounts with a backup");
|
|
SaveDataEvent event{};
|
|
Check(sceSaveDataGetEventResult(nullptr, &event) == 0, name + "'s backup posts an event");
|
|
return event.error_code;
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
const char* failingStep = nullptr;
|
|
int failingCall = 0;
|
|
int stepCalls = 0;
|
|
|
|
bool FailingStep(const char* step, const char*) {
|
|
if (failingStep == nullptr || std::strcmp(step, failingStep) != 0) return true;
|
|
return ++stepCalls != failingCall;
|
|
}
|
|
|
|
void FailStep(const char* step, int call) {
|
|
failingStep = step;
|
|
failingCall = call;
|
|
stepCalls = 0;
|
|
SaveDataSetFileStepHook_nid_no_patch(FailingStep);
|
|
}
|
|
|
|
void ClearFailingStep() {
|
|
SaveDataSetFileStepHook_nid_no_patch(nullptr);
|
|
failingStep = nullptr;
|
|
}
|
|
|
|
struct FailedStep {
|
|
const char* step;
|
|
int call;
|
|
};
|
|
|
|
bool LeavesCopies(const std::string& name) {
|
|
for (const char* root : {"_sd_backup_tmp", "_sd_backup_old", "_sd_restore_tmp", "_sd_restore_old"}) {
|
|
if (std::filesystem::exists(std::filesystem::path(root) / name)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
template <typename Damage>
|
|
void CheckCorruptBackup(const std::string& label, Damage damage) {
|
|
SceSaveDataDirName name{};
|
|
std::memcpy(name.data, "corrupt", sizeof("corrupt"));
|
|
Check(BackUp(&name, "backed up", "Corrupt") == 0, label + ": the save is backed up");
|
|
WriteText(std::filesystem::path("_sd") / "corrupt" / "data.bin", "live");
|
|
damage(std::filesystem::path("_sd_backup") / "corrupt");
|
|
const auto live = std::filesystem::path("_sd") / "corrupt";
|
|
const auto before = Snapshot(live);
|
|
SaveDataCheckBackupData check{};
|
|
check.dir_name = &name;
|
|
Check(sceSaveDataCheckBackupData(&check) == SaveDataErrorInternal, label + ": checkBackupData without param is INTERNAL");
|
|
SaveDataParam param;
|
|
std::memset(¶m, 0xA5, sizeof(param));
|
|
const SaveDataParam untouched = param;
|
|
check.param = ¶m;
|
|
Check(sceSaveDataCheckBackupData(&check) == SaveDataErrorInternal, label + ": checkBackupData with param is INTERNAL");
|
|
Check(std::memcmp(¶m, &untouched, sizeof(param)) == 0, label + ": checkBackupData leaves param as it was");
|
|
SaveDataRestoreBackupData restore{};
|
|
restore.dir_name = &name;
|
|
Check(sceSaveDataRestoreBackupData(&restore) == SaveDataErrorInternal, label + ": restoreBackupData is INTERNAL");
|
|
Check(Snapshot(live) == before, label + ": restoreBackupData leaves the save byte for byte");
|
|
}
|
|
|
|
void CheckCorruptBackups() {
|
|
CheckCorruptBackup("a backup without its .param", [](const std::filesystem::path& entry) { std::filesystem::remove(entry / "save.param"); });
|
|
CheckCorruptBackup("an empty backup directory", [](const std::filesystem::path& entry) {
|
|
std::filesystem::remove_all(entry);
|
|
std::filesystem::create_directory(entry);
|
|
});
|
|
CheckCorruptBackup("a backup with a short .param", [](const std::filesystem::path& entry) {
|
|
const std::string bytes = ReadText(entry / "save.param");
|
|
WriteText(entry / "save.param", bytes.substr(0, bytes.size() - 1));
|
|
});
|
|
CheckCorruptBackup("a backup with a long .param", [](const std::filesystem::path& entry) { WriteText(entry / "save.param", ReadText(entry / "save.param") + "x"); });
|
|
}
|
|
|
|
void CheckStagedRestore() {
|
|
SceSaveDataDirName name{};
|
|
std::memcpy(name.data, "staged", sizeof("staged"));
|
|
const auto live = std::filesystem::path("_sd") / "staged";
|
|
std::filesystem::create_directories(live / "sub");
|
|
WriteText(live / "sub" / "deep.bin", "deep");
|
|
WriteText(std::filesystem::path("_sd") / "staged.blocks", "96");
|
|
Check(BackUp(&name, "backed up", "Staged") == 0, "the staged save is backed up");
|
|
const auto backup = Snapshot(std::filesystem::path("_sd_backup") / "staged" / "save");
|
|
WriteText(live / "data.bin", "live");
|
|
WriteText(live / "added.bin", "added");
|
|
WriteText(std::filesystem::path("_sd") / "staged.blocks", "128");
|
|
const auto before = Snapshot(live);
|
|
SaveDataRestoreBackupData restore{};
|
|
restore.dir_name = &name;
|
|
const FailedStep failures[] = {{"restore.copy", 1}, {"restore.copy", 3}, {"restore.copy", 6}, {"restore.aside", 1}, {"restore.aside", 2}, {"restore.place", 1}, {"restore.place", 3}};
|
|
for (const FailedStep& failure : failures) {
|
|
const std::string what = std::string("a restore whose ") + failure.step + " step " + std::to_string(failure.call) + " fails";
|
|
FailStep(failure.step, failure.call);
|
|
Check(sceSaveDataRestoreBackupData(&restore) == SaveDataErrorInternal, what + " is INTERNAL");
|
|
ClearFailingStep();
|
|
Check(Snapshot(live) == before, what + " leaves the save as it was");
|
|
Check(!LeavesCopies("staged"), what + " leaves no staged or old copy");
|
|
}
|
|
Check(sceSaveDataRestoreBackupData(&restore) == 0, "a restore with no failing step succeeds");
|
|
Check(Snapshot(live) == backup, "the restored save matches its backup byte for byte");
|
|
Check(!LeavesCopies("staged"), "a restore leaves no staged or old copy");
|
|
std::filesystem::remove_all(live);
|
|
Check(sceSaveDataRestoreBackupData(&restore) == SaveDataErrorInternal, "a restore into a missing save directory is INTERNAL");
|
|
SaveDataCheckBackupData check{};
|
|
check.dir_name = &name;
|
|
Check(sceSaveDataCheckBackupData(&check) == 0, "the backup outlives a restore into a missing save directory");
|
|
}
|
|
|
|
void CheckBackupSwap() {
|
|
SceSaveDataDirName name{};
|
|
std::memcpy(name.data, "rotated", sizeof("rotated"));
|
|
const auto backup = std::filesystem::path("_sd_backup") / "rotated" / "save";
|
|
SaveDataParam param{};
|
|
SaveDataCheckBackupData check{};
|
|
check.dir_name = &name;
|
|
check.param = ¶m;
|
|
const char* titles[] = {"First", "Second", "Third"};
|
|
for (int i = 0; i < 3; ++i) {
|
|
const std::string what = "backup " + std::to_string(i + 1) + " of three";
|
|
const std::string data = "rotation " + std::to_string(i + 1);
|
|
Check(BackUp(&name, data, titles[i]) == 0, what + " succeeds");
|
|
Check(ReadText(backup / "data.bin") == data, what + " holds the save it was made from");
|
|
Check(sceSaveDataCheckBackupData(&check) == 0 && std::strcmp(param.title, titles[i]) == 0, what + " replaces the last one's param");
|
|
Check(!LeavesCopies("rotated"), what + " leaves no staged or old copy");
|
|
}
|
|
const auto previous = Snapshot(backup);
|
|
const FailedStep failures[] = {{"backup.aside", 1}, {"backup.place", 1}};
|
|
for (const FailedStep& failure : failures) {
|
|
const std::string what = std::string("a backup whose ") + failure.step + " step fails";
|
|
FailStep(failure.step, failure.call);
|
|
const int error = BackUp(&name, "unsaved", "Unsaved");
|
|
ClearFailingStep();
|
|
Check(error == SaveDataErrorInternal, what + " gives the event error INTERNAL, got " + std::to_string(error));
|
|
Check(Snapshot(backup) == previous, what + " keeps the previous backup byte for byte");
|
|
param = {};
|
|
Check(sceSaveDataCheckBackupData(&check) == 0 && std::strcmp(param.title, "Third") == 0, what + " leaves the previous backup usable");
|
|
Check(!LeavesCopies("rotated"), what + " leaves no staged or old copy");
|
|
}
|
|
SaveDataRestoreBackupData restore{};
|
|
restore.dir_name = &name;
|
|
Check(sceSaveDataRestoreBackupData(&restore) == 0, "the backup kept by the failed backups restores");
|
|
Check(ReadText(std::filesystem::path("_sd") / "rotated" / "data.bin") == "rotation 3", "the restore puts back the third backup");
|
|
}
|
|
|
|
}
|
|
|
|
int main() {
|
|
const auto root = std::filesystem::temp_directory_path() / ("anyps5-savedata-mount-status-" + std::to_string(std::random_device{}()));
|
|
std::filesystem::create_directories(root);
|
|
const auto previous = std::filesystem::current_path();
|
|
std::filesystem::current_path(root);
|
|
CheckUnmountedCalls();
|
|
Check(sceSaveDataSyncSaveDataMemory(nullptr) == static_cast<int>(0x809F0000), "null param rejected in memory sync");
|
|
Check(sceSaveDataInitialize3(nullptr) == 0, "SaveData initializes");
|
|
MountAndCheck("fresh", MountReadWrite | MountCreate2, 1u, "CREATE2 of a missing save");
|
|
MountAndCheck("fresh", MountReadWrite | MountCreate2, 0u, "CREATE2 of an existing save");
|
|
MountAndCheck("made", MountReadWrite | MountCreate, 1u, "CREATE of a missing save");
|
|
MountAndCheck("made", MountReadWrite, 0u, "open of an existing save");
|
|
CheckFailedMountKeepsResult();
|
|
CheckEquivalentMounts();
|
|
CheckInvalidNamesAreRejected();
|
|
CheckBackups();
|
|
CheckCorruptBackups();
|
|
CheckStagedRestore();
|
|
CheckBackupSwap();
|
|
std::filesystem::current_path(previous);
|
|
std::error_code error;
|
|
std::filesystem::remove_all(root, error);
|
|
if (failures != 0) return 1;
|
|
std::printf("savedata mount status tests passed\n");
|
|
return 0;
|
|
}
|