627 lines
23 KiB
C++
627 lines
23 KiB
C++
// Exercise the guest ABI without linking unrelated emulator subsystems.
|
|
#include "libs/libSaveData.cpp"
|
|
|
|
#include <array>
|
|
#include <chrono>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <limits>
|
|
|
|
#ifdef _WIN32
|
|
#include <process.h>
|
|
#else
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
namespace {
|
|
std::string g_test_title = "PPSA21564";
|
|
std::filesystem::path g_mounted_directory;
|
|
std::map<std::string, std::filesystem::path> g_test_mounts;
|
|
} // namespace
|
|
|
|
namespace Loader {
|
|
bool SystemContentParamSfoGetString(const char*, std::string* value) {
|
|
*value = g_test_title;
|
|
return true;
|
|
}
|
|
void SymbolDatabase::Add(const SymbolResolve&, uint64_t, const std::string&) {}
|
|
namespace Timer {
|
|
double GetTimeMs() {
|
|
return 0.0;
|
|
}
|
|
} // namespace Timer
|
|
} // namespace Loader
|
|
|
|
namespace Libs::LibKernel::FileSystem {
|
|
void Mount(const std::filesystem::path& directory, const std::string& point) {
|
|
g_mounted_directory = directory;
|
|
g_test_mounts[point] = directory;
|
|
}
|
|
void Umount(const std::string& point) { g_test_mounts.erase(point); }
|
|
std::filesystem::path GetRealFilename(const std::string& point) {
|
|
return g_test_mounts.at(point);
|
|
}
|
|
} // namespace Libs::LibKernel::FileSystem
|
|
|
|
namespace {
|
|
using namespace Libs::SaveData;
|
|
namespace fs = std::filesystem;
|
|
|
|
#define CHECK(condition) \
|
|
do { \
|
|
if (!(condition)) { \
|
|
std::fprintf(stderr, "SaveDataMemoryTests:%d: %s\n", __LINE__, #condition); \
|
|
std::abort(); \
|
|
} \
|
|
} while (false)
|
|
|
|
SaveDataMemorySetup2 SetupParam(int32_t user = 1, uint32_t slot = 0, size_t size = 16) {
|
|
SaveDataMemorySetup2 setup {};
|
|
setup.user_id = user;
|
|
setup.slot_id = slot;
|
|
setup.memory_size = size;
|
|
return setup;
|
|
}
|
|
|
|
size_t Setup(int32_t user = 1, uint32_t slot = 0, size_t size = 16, uint32_t option = 0) {
|
|
auto setup = SetupParam(user, slot, size);
|
|
setup.option = option;
|
|
SaveDataMemorySetupResult result {};
|
|
CHECK(SaveDataSetupSaveDataMemory2(&setup, &result) == OK);
|
|
return result.existed_memory_size;
|
|
}
|
|
|
|
int Set(void* bytes, size_t size, int64_t offset = 0, int32_t user = 1, uint32_t slot = 0) {
|
|
SaveDataMemoryData data {bytes, size, offset, {}};
|
|
SaveDataMemorySet2 set {};
|
|
set.user_id = user;
|
|
set.slot_id = slot;
|
|
set.data = &data;
|
|
set.data_num = 1;
|
|
return SaveDataSetSaveDataMemory2(&set);
|
|
}
|
|
|
|
std::vector<uint8_t> Get(size_t size = 16, int32_t user = 1, uint32_t slot = 0) {
|
|
std::vector<uint8_t> bytes(size);
|
|
SaveDataMemoryData data {bytes.data(), size, 0, {}};
|
|
SaveDataMemoryGet2 get {};
|
|
get.user_id = user;
|
|
get.slot_id = slot;
|
|
get.data = &data;
|
|
CHECK(SaveDataGetSaveDataMemory2(&get) == OK);
|
|
return bytes;
|
|
}
|
|
|
|
void Reset(const char* title) {
|
|
CHECK(SaveDataTerminate() == OK);
|
|
g_test_title = title;
|
|
}
|
|
|
|
void TestValidationAndScatter() {
|
|
Reset("VALIDATE");
|
|
uint8_t byte = 0x55;
|
|
SaveDataMemoryGet2 get {};
|
|
get.user_id = 1;
|
|
SaveDataMemorySync sync {};
|
|
sync.user_id = 1;
|
|
CHECK(SaveDataGetSaveDataMemory2(&get) == SAVE_DATA_ERROR_MEMORY_NOT_READY);
|
|
CHECK(Set(&byte, 1) == SAVE_DATA_ERROR_MEMORY_NOT_READY);
|
|
CHECK(SaveDataSyncSaveDataMemory(&sync) == SAVE_DATA_ERROR_MEMORY_NOT_READY);
|
|
CHECK(SaveDataSetupSaveDataMemory2(nullptr, nullptr) == SAVE_DATA_ERROR_PARAMETER);
|
|
auto invalid_user = SetupParam(-1);
|
|
CHECK(SaveDataSetupSaveDataMemory2(&invalid_user, nullptr) ==
|
|
SAVE_DATA_ERROR_INVALID_LOGIN_USER);
|
|
for (auto invalid:
|
|
{SetupParam(1, 4), SetupParam(1, 0, 0), SetupParam(1, 0, 32 * 1024 * 1024 + 1)}) {
|
|
CHECK(SaveDataSetupSaveDataMemory2(&invalid, nullptr) == SAVE_DATA_ERROR_PARAMETER);
|
|
}
|
|
CHECK(Setup() == 0);
|
|
auto setup = SetupParam();
|
|
CHECK(SaveDataSetupSaveDataMemory2(&setup, nullptr) == SAVE_DATA_ERROR_BUSY);
|
|
std::array<uint8_t, 16> original {};
|
|
for (size_t i = 0; i < original.size(); ++i) {
|
|
original[i] = static_cast<uint8_t>(i);
|
|
}
|
|
CHECK(Set(original.data(), original.size()) == OK);
|
|
SaveDataMemoryData data[] = {{&byte, 1, 2, {}}, {&byte, 1, 16, {}}};
|
|
SaveDataMemorySet2 set {};
|
|
set.user_id = 1;
|
|
set.data = data;
|
|
set.data_num = 2;
|
|
CHECK(SaveDataSetSaveDataMemory2(&set) == SAVE_DATA_ERROR_PARAMETER);
|
|
CHECK(Get() == std::vector<uint8_t>(original.begin(), original.end()));
|
|
data[1].offset = 12;
|
|
CHECK(SaveDataSetSaveDataMemory2(&set) == OK);
|
|
original[2] = original[12] = byte;
|
|
CHECK(Get() == std::vector<uint8_t>(original.begin(), original.end()));
|
|
|
|
byte = 0xaa;
|
|
set.data_num = 0;
|
|
CHECK(SaveDataSetSaveDataMemory2(&set) == OK);
|
|
original[2] = byte;
|
|
CHECK(Get() == std::vector<uint8_t>(original.begin(), original.end()));
|
|
set.data = nullptr;
|
|
CHECK(SaveDataSetSaveDataMemory2(&set) == OK);
|
|
CHECK(Get() == std::vector<uint8_t>(original.begin(), original.end()));
|
|
set.data_num = 6;
|
|
CHECK(SaveDataSetSaveDataMemory2(&set) == SAVE_DATA_ERROR_PARAMETER);
|
|
set.data_num = 1;
|
|
set.data = nullptr;
|
|
CHECK(SaveDataSetSaveDataMemory2(&set) == SAVE_DATA_ERROR_PARAMETER);
|
|
CHECK(Set(&byte, 1, -1) == SAVE_DATA_ERROR_PARAMETER);
|
|
CHECK(Set(&byte, 1, std::numeric_limits<int64_t>::max()) == SAVE_DATA_ERROR_PARAMETER);
|
|
CHECK(Set(&byte, std::numeric_limits<size_t>::max(), 1) == SAVE_DATA_ERROR_PARAMETER);
|
|
SaveDataMemoryData invalid {&byte, std::numeric_limits<size_t>::max(), 1, {}};
|
|
get.data = &invalid;
|
|
CHECK(SaveDataGetSaveDataMemory2(&get) == SAVE_DATA_ERROR_PARAMETER);
|
|
CHECK(byte == 0xaa);
|
|
CHECK(Get() == std::vector<uint8_t>(original.begin(), original.end()));
|
|
CHECK(SaveDataTerminate() == OK);
|
|
CHECK(Setup() == original.size());
|
|
CHECK(Get() == std::vector<uint8_t>(original.begin(), original.end()));
|
|
}
|
|
|
|
void TestIsolationAndSync() {
|
|
Reset("ISOLATE");
|
|
uint8_t byte = 11;
|
|
CHECK(Setup() == 0);
|
|
CHECK(Set(&byte, 1) == OK);
|
|
CHECK(Setup(2) == 0);
|
|
CHECK(Setup(1, 1) == 0);
|
|
CHECK(Get(16, 2) == std::vector<uint8_t>(16));
|
|
CHECK(Get(16, 1, 1) == std::vector<uint8_t>(16));
|
|
byte = 22;
|
|
CHECK(Set(&byte, 1, 0, 2) == OK);
|
|
byte = 33;
|
|
CHECK(Set(&byte, 1, 0, 1, 1) == OK);
|
|
CHECK(Get()[0] == 11);
|
|
CHECK(Get(16, 2)[0] == 22);
|
|
CHECK(Get(16, 1, 1)[0] == 33);
|
|
|
|
SaveDataMemorySync sync {};
|
|
sync.user_id = 1;
|
|
sync.slot_id = 1;
|
|
CHECK(SaveDataSyncSaveDataMemory(&sync) == OK);
|
|
SaveDataEvent event {};
|
|
CHECK(SaveDataGetEventResult(nullptr, &event) == OK);
|
|
CHECK(event.type == 3 && event.error_code == OK && event.user_id == 1);
|
|
CHECK(std::string(event.dir_name.data) == "sce_sdmemory1");
|
|
CHECK(SaveDataGetEventResult(nullptr, &event) == SAVE_DATA_ERROR_NOT_FOUND);
|
|
sync.option = 1;
|
|
CHECK(SaveDataSyncSaveDataMemory(&sync) == OK);
|
|
CHECK(SaveDataGetEventResult(nullptr, &event) == SAVE_DATA_ERROR_NOT_FOUND);
|
|
sync.option = 2;
|
|
CHECK(SaveDataSyncSaveDataMemory(&sync) == SAVE_DATA_ERROR_PARAMETER);
|
|
|
|
Reset("OTHER");
|
|
CHECK(Setup() == 0);
|
|
CHECK(Get() == std::vector<uint8_t>(16));
|
|
Reset("ISOLATE");
|
|
CHECK(Setup() == 16);
|
|
CHECK(Setup(2) == 16);
|
|
CHECK(Setup(1, 1) == 16);
|
|
CHECK(Get()[0] == 11 && Get(16, 2)[0] == 22 && Get(16, 1, 1)[0] == 33);
|
|
Reset("OTHER");
|
|
SceSaveDataTitleId title {};
|
|
std::strcpy(title.data, "ISOLATE");
|
|
SceSaveDataDirName name {};
|
|
std::strcpy(name.data, "sce_sdmemory");
|
|
struct SaveDataTransferringMount mount {};
|
|
mount.user_id = 2;
|
|
mount.title_id = &title;
|
|
mount.dir_name = &name;
|
|
SaveDataMountResult result {};
|
|
CHECK(SaveDataTransferringMount(&mount, &result) == OK);
|
|
CHECK(g_mounted_directory == fs::path("_SaveData/ISOLATE/sce_sdmemory/2"));
|
|
CHECK(std::ifstream(g_mounted_directory / "memory.dat", std::ios::binary).get() == 22);
|
|
CHECK(SaveDataUmount2(0, &result.mount_point) == OK);
|
|
struct SaveDataDelete del {};
|
|
del.user_id = 1;
|
|
del.title_id = &title;
|
|
del.dir_name = &name;
|
|
CHECK(SaveDataDelete(&del) == OK);
|
|
Reset("ISOLATE");
|
|
CHECK(Setup() == 0);
|
|
CHECK(Setup(2) == 16);
|
|
CHECK(Get(16, 2)[0] == 22);
|
|
}
|
|
|
|
void TestFailedWritePreservesSave() {
|
|
Reset("IOFAIL");
|
|
CHECK(Setup() == 0);
|
|
uint8_t byte = 42;
|
|
CHECK(Set(&byte, 1) == OK);
|
|
fs::rename("_SaveData", "save-backup");
|
|
{
|
|
std::ofstream blocker("_SaveData");
|
|
blocker << "not a directory";
|
|
}
|
|
byte = 99;
|
|
CHECK(Set(&byte, 1) < 0);
|
|
CHECK(Get()[0] == 42);
|
|
CHECK(fs::remove("_SaveData"));
|
|
fs::rename("save-backup", "_SaveData");
|
|
CHECK(SaveDataTerminate() == OK);
|
|
CHECK(Setup() == 16);
|
|
CHECK(Get()[0] == 42);
|
|
}
|
|
|
|
void WriteRestartFixture() {
|
|
auto setup = SetupParam(7, 3);
|
|
setup.option = 1;
|
|
std::array<uint8_t, 4> icon_bytes {1, 2, 3, 4};
|
|
SaveDataIcon icon {icon_bytes.data(), icon_bytes.size(), icon_bytes.size(), {}};
|
|
setup.init_icon = &icon;
|
|
setup.icon_memory_size = icon_bytes.size();
|
|
SaveDataParam param {};
|
|
std::strcpy(param.title, "Astro progress");
|
|
param.user_param = 17;
|
|
setup.init_param = ¶m;
|
|
CHECK(SaveDataSetupSaveDataMemory2(&setup, nullptr) == OK);
|
|
std::array<uint8_t, 4> progress {11, 22, 33, 44};
|
|
SaveDataMemoryData data {progress.data(), progress.size(), 4, {}};
|
|
SaveDataMemorySet2 set {};
|
|
set.user_id = 7;
|
|
set.slot_id = 3;
|
|
set.data = &data;
|
|
set.icon = &icon;
|
|
CHECK(SaveDataSetSaveDataMemory2(&set) == OK);
|
|
set.data = nullptr;
|
|
param.user_param = 18;
|
|
std::strcpy(param.detail, "Coins, level state, rescued bots");
|
|
set.param = ¶m;
|
|
CHECK(SaveDataSetSaveDataMemory2(&set) == OK);
|
|
// No Sync or Terminate: Set must survive process exit on its own.
|
|
}
|
|
|
|
void ReadRestartFixture() {
|
|
CHECK(Setup(7, 3, 24, 1) == 16);
|
|
auto expected = std::vector<uint8_t>(24);
|
|
expected[4] = 11;
|
|
expected[5] = 22;
|
|
expected[6] = 33;
|
|
expected[7] = 44;
|
|
CHECK(Get(24, 7, 3) == expected);
|
|
SaveDataParam param {};
|
|
std::array<uint8_t, 4> icon_bytes {9, 9, 9, 9};
|
|
SaveDataIcon icon {icon_bytes.data(), icon_bytes.size(), icon_bytes.size(), {}};
|
|
SaveDataMemoryGet2 get {};
|
|
get.user_id = 7;
|
|
get.slot_id = 3;
|
|
get.param = ¶m;
|
|
get.icon = &icon;
|
|
CHECK(SaveDataGetSaveDataMemory2(&get) == OK);
|
|
CHECK(icon.data_size == 0);
|
|
CHECK(
|
|
std::all_of(icon_bytes.begin(), icon_bytes.end(), [](uint8_t byte) { return byte == 9; }));
|
|
CHECK(std::string(param.title) == "Astro progress");
|
|
CHECK(std::string(param.detail) == "Coins, level state, rescued bots");
|
|
CHECK(param.user_param == 18);
|
|
CHECK(SaveDataTerminate() == OK);
|
|
CHECK(Setup(7, 3, 6) == 16);
|
|
expected.resize(6);
|
|
CHECK(Get(6, 7, 3) == expected);
|
|
}
|
|
|
|
SceSaveDataDirName DirName(const char* text) {
|
|
SceSaveDataDirName name {};
|
|
std::snprintf(name.data, sizeof(name.data), "%s", text);
|
|
return name;
|
|
}
|
|
|
|
std::vector<std::string> Search(int32_t user, const SceSaveDataTitleId* title = nullptr) {
|
|
SaveDataDirNameSearchCond cond {};
|
|
cond.user_id = user;
|
|
cond.title_id = title;
|
|
std::array<SceSaveDataDirName, 8> names {};
|
|
SaveDataDirNameSearchResult result {};
|
|
result.dir_names = names.data();
|
|
result.dir_names_num = names.size();
|
|
CHECK(SaveDataDirNameSearch(&cond, &result) == OK);
|
|
CHECK(result.hit_num == result.set_num && result.set_num <= names.size());
|
|
std::vector<std::string> found;
|
|
for (uint32_t i = 0; i < result.set_num; ++i) {
|
|
found.emplace_back(names[i].data);
|
|
}
|
|
return found;
|
|
}
|
|
|
|
void TestClassicSavePaths() {
|
|
Reset("CLASSIC");
|
|
const std::vector<std::string> names {"save.1", "save1", "slot@A", "slotA"};
|
|
const auto existing = DirName("save.1");
|
|
struct SaveDataMount3 initial {};
|
|
initial.user_id = 1;
|
|
initial.dir_name = &existing;
|
|
initial.mount_mode = 4;
|
|
initial.blocks = 48;
|
|
SaveDataMountResult first {};
|
|
CHECK(SaveDataMount3(&initial, &first) == OK);
|
|
std::ofstream(g_mounted_directory / "progress") << "save.1";
|
|
CHECK(SaveDataUmount2(0, &first.mount_point) == OK);
|
|
CHECK(Search(1) == std::vector<std::string> {"save.1"});
|
|
for (const auto& text: names) {
|
|
auto name = DirName(text.c_str());
|
|
struct SaveDataMount3 mount {};
|
|
mount.user_id = 1;
|
|
mount.dir_name = &name;
|
|
mount.mount_mode = text == "save.1" ? 1 : 4;
|
|
mount.blocks = 48;
|
|
SaveDataMountResult result {};
|
|
CHECK(SaveDataMount3(&mount, &result) == OK);
|
|
CHECK(g_mounted_directory == fs::path("_SaveData") / "CLASSIC" / text);
|
|
if (text != "save.1") {
|
|
std::ofstream saved(g_mounted_directory / "progress");
|
|
saved << text;
|
|
}
|
|
CHECK(SaveDataUmount2(0, &result.mount_point) == OK);
|
|
}
|
|
CHECK(Search(1) == names);
|
|
SceSaveDataTitleId title {};
|
|
std::strcpy(title.data, "CLASSIC");
|
|
Reset("OTHER");
|
|
CHECK(Search(1).empty());
|
|
CHECK(Search(1, &title) == names);
|
|
for (const auto& text: names) {
|
|
auto name = DirName(text.c_str());
|
|
struct SaveDataTransferringMount mount {};
|
|
mount.user_id = 1;
|
|
mount.title_id = &title;
|
|
mount.dir_name = &name;
|
|
SaveDataMountResult result {};
|
|
CHECK(SaveDataTransferringMount(&mount, &result) == OK);
|
|
std::ifstream saved(g_mounted_directory / "progress");
|
|
std::string progress;
|
|
saved >> progress;
|
|
CHECK(progress == text);
|
|
CHECK(SaveDataUmount2(0, &result.mount_point) == OK);
|
|
}
|
|
Reset("CLASSIC");
|
|
for (const char* text: {"", ".", "..", "../save1", "save/1", "save\\1"}) {
|
|
auto name = DirName(text);
|
|
struct SaveDataMount3 mount {};
|
|
mount.user_id = 1;
|
|
mount.dir_name = &name;
|
|
mount.mount_mode = 32;
|
|
SaveDataMountResult result {};
|
|
g_mounted_directory.clear();
|
|
CHECK(SaveDataMount3(&mount, &result) == SAVE_DATA_ERROR_PARAMETER);
|
|
CHECK(g_mounted_directory.empty());
|
|
struct SaveDataDelete del {};
|
|
del.user_id = 1;
|
|
del.dir_name = &name;
|
|
CHECK(SaveDataDelete(&del) == SAVE_DATA_ERROR_PARAMETER);
|
|
}
|
|
CHECK(Search(1) == names);
|
|
}
|
|
|
|
void TestSaveAllocations() {
|
|
Reset("CAPACITY");
|
|
std::array<SceSaveDataDirName, 2> names {DirName("Options"), DirName("Player")};
|
|
const std::array<uint64_t, 2> allocations {48, 96};
|
|
for (const auto blocks : {0u, 47u, 16385u}) {
|
|
struct SaveDataMount3 invalid {};
|
|
invalid.user_id = 1;
|
|
invalid.dir_name = &names[0];
|
|
invalid.mount_mode = 4;
|
|
invalid.blocks = blocks;
|
|
SaveDataMountResult result {};
|
|
CHECK(SaveDataMount3(&invalid, &result) == SAVE_DATA_ERROR_PARAMETER);
|
|
CHECK(!fs::exists("_SaveData/CAPACITY/Options"));
|
|
}
|
|
for (size_t i = 0; i < names.size(); i++) {
|
|
struct SaveDataMount3 mount {};
|
|
mount.user_id = 1;
|
|
mount.dir_name = &names[i];
|
|
mount.mount_mode = 4;
|
|
mount.blocks = allocations[i];
|
|
SaveDataMountResult result {};
|
|
CHECK(SaveDataMount3(&mount, &result) == OK);
|
|
CHECK(result.mount_status == 1);
|
|
std::ofstream(g_mounted_directory / "USR-DATA") << "saved payload";
|
|
CHECK(SaveDataUmount2(0, &result.mount_point) == OK);
|
|
SaveDataMountInfo info {};
|
|
CHECK(SaveDataGetMountInfo(&result.mount_point, &info) == SAVE_DATA_ERROR_NOT_MOUNTED);
|
|
}
|
|
Reset("CAPACITY");
|
|
std::array<SceSaveDataDirName, 2> found {};
|
|
std::array<SaveDataSearchInfo, 2> infos {};
|
|
SaveDataDirNameSearchCond cond {};
|
|
cond.user_id = 1;
|
|
SaveDataDirNameSearchResult search {};
|
|
search.dir_names = found.data();
|
|
search.dir_names_num = found.size();
|
|
search.infos = infos.data();
|
|
CHECK(SaveDataDirNameSearch(&cond, &search) == OK && search.set_num == 2);
|
|
for (size_t i = 0; i < names.size(); i++) {
|
|
CHECK(std::string(found[i].data) == names[i].data);
|
|
CHECK(infos[i].blocks == allocations[i] && infos[i].free_blocks == allocations[i]);
|
|
struct SaveDataMount3 mount {};
|
|
mount.user_id = 1;
|
|
mount.dir_name = &names[i];
|
|
mount.mount_mode = 34;
|
|
mount.blocks = 16384;
|
|
SaveDataMountResult result {};
|
|
CHECK(SaveDataMount3(&mount, &result) == OK && result.mount_status == 0);
|
|
std::ofstream(g_mounted_directory / "USR-DATA", std::ios::app) << "more data";
|
|
SaveDataMountInfo info {};
|
|
CHECK(SaveDataGetMountInfo(&result.mount_point, &info) == OK);
|
|
CHECK(info.blocks == allocations[i] && info.free_blocks == allocations[i]);
|
|
CHECK(SaveDataUmount2(0, &result.mount_point) == OK);
|
|
}
|
|
CHECK(infos[0].blocks + infos[1].blocks == 144);
|
|
const fs::path metadata = "_SaveData/CAPACITY/Options/sce_sys/blocks.bin";
|
|
fs::resize_file(metadata, 7);
|
|
CHECK(SaveDataDirNameSearch(&cond, &search) == SAVE_DATA_ERROR_BROKEN);
|
|
CHECK(fs::file_size(metadata) == 7);
|
|
CHECK(fs::remove(metadata));
|
|
CHECK(SaveDataDirNameSearch(&cond, &search) == SAVE_DATA_ERROR_BROKEN);
|
|
CHECK(!fs::exists(metadata));
|
|
}
|
|
|
|
void TestClassicSaveParams() {
|
|
Reset("PARAMS");
|
|
const auto name = DirName("slot");
|
|
struct SaveDataMount3 mount {};
|
|
mount.user_id = 1;
|
|
mount.dir_name = &name;
|
|
mount.mount_mode = 4;
|
|
mount.blocks = 48;
|
|
SaveDataMountResult mounted {};
|
|
CHECK(SaveDataMount3(&mount, &mounted) == OK);
|
|
SaveDataParam param {};
|
|
std::strcpy(param.title, "First save");
|
|
std::strcpy(param.sub_title, "Chapter 2");
|
|
std::strcpy(param.detail, "Progress");
|
|
param.user_param = 42;
|
|
CHECK(SaveDataSetParam(&mounted.mount_point, 0, nullptr, sizeof(param)) ==
|
|
SAVE_DATA_ERROR_PARAMETER);
|
|
CHECK(SaveDataSetParam(&mounted.mount_point, 0, ¶m, sizeof(param) - 1) ==
|
|
SAVE_DATA_ERROR_PARAMETER);
|
|
CHECK(SaveDataSetParam(&mounted.mount_point, 0, ¶m, sizeof(param)) == OK);
|
|
const fs::path saved = "_SaveData/PARAMS/slot";
|
|
CHECK(fs::exists(saved / "sce_sys/param.bin"));
|
|
fs::create_directory(saved / "nested");
|
|
std::ofstream(saved / "nested/progress") << "payload";
|
|
SaveDataParam loaded {};
|
|
size_t got = 0;
|
|
CHECK(SaveDataGetParam(&mounted.mount_point, 0, &loaded, sizeof(loaded), &got) == OK);
|
|
CHECK(got == sizeof(loaded) && std::string(loaded.title) == "First save");
|
|
CHECK(std::string(loaded.sub_title) == "Chapter 2" && loaded.user_param == 42);
|
|
CHECK(loaded.mtime > 0);
|
|
fs::last_write_time(saved / "nested/progress",
|
|
fs::last_write_time(saved / "sce_sys/param.bin") + std::chrono::hours(2));
|
|
CHECK(SaveDataGetParam(&mounted.mount_point, 0, &loaded, sizeof(loaded), nullptr) == OK);
|
|
const int64_t nested_mtime = loaded.mtime;
|
|
CHECK(nested_mtime > static_cast<int64_t>(std::time(nullptr)) + 3600);
|
|
fs::create_directory(saved / "sce_sys/param.bin.tmp");
|
|
std::strcpy(param.title, "Failed update");
|
|
CHECK(SaveDataSetParam(&mounted.mount_point, 0, ¶m, sizeof(param)) ==
|
|
SAVE_DATA_ERROR_INTERNAL);
|
|
CHECK(fs::remove(saved / "sce_sys/param.bin.tmp"));
|
|
CHECK(SaveDataGetParam(&mounted.mount_point, 0, &loaded, sizeof(loaded), nullptr) == OK);
|
|
CHECK(std::string(loaded.title) == "First save");
|
|
std::strcpy(param.title, "Updated save");
|
|
CHECK(SaveDataSetParam(&mounted.mount_point, 0, ¶m, sizeof(param)) == OK);
|
|
CHECK(SaveDataGetParam(&mounted.mount_point, 0, &loaded, sizeof(loaded), nullptr) == OK);
|
|
CHECK(std::string(loaded.title) == "Updated save");
|
|
CHECK(SaveDataUmount2(0, &mounted.mount_point) == OK);
|
|
CHECK(SaveDataGetParam(&mounted.mount_point, 0, &loaded, sizeof(loaded), nullptr) ==
|
|
SAVE_DATA_ERROR_NOT_MOUNTED);
|
|
|
|
Reset("OTHER");
|
|
SceSaveDataTitleId source_title {};
|
|
std::strcpy(source_title.data, "PARAMS");
|
|
struct SaveDataTransferringMount transfer {};
|
|
transfer.user_id = 1;
|
|
transfer.title_id = &source_title;
|
|
transfer.dir_name = &name;
|
|
SaveDataMountResult transferred {};
|
|
CHECK(SaveDataTransferringMount(&transfer, &transferred) == OK);
|
|
CHECK(SaveDataGetParam(&transferred.mount_point, 0, &loaded, sizeof(loaded), nullptr) == OK);
|
|
CHECK(std::string(loaded.title) == "Updated save");
|
|
std::error_code alias_error;
|
|
fs::create_directory_symlink("PARAMS", "_SaveData/P_ALIAS", alias_error);
|
|
if (!alias_error) {
|
|
SceSaveDataTitleId alias_title {};
|
|
std::strcpy(alias_title.data, "P_ALIAS");
|
|
transfer.title_id = &alias_title;
|
|
SaveDataMountResult duplicate {};
|
|
CHECK(SaveDataTransferringMount(&transfer, &duplicate) == SAVE_DATA_ERROR_BUSY);
|
|
transfer.title_id = &source_title;
|
|
CHECK(fs::remove("_SaveData/P_ALIAS"));
|
|
}
|
|
#ifdef _WIN32
|
|
SceSaveDataTitleId lowercase_title {};
|
|
std::strcpy(lowercase_title.data, "params");
|
|
transfer.title_id = &lowercase_title;
|
|
SaveDataMountResult duplicate {};
|
|
CHECK(SaveDataTransferringMount(&transfer, &duplicate) == SAVE_DATA_ERROR_BUSY);
|
|
transfer.title_id = &source_title;
|
|
#endif
|
|
|
|
SaveDataMountResult other {};
|
|
CHECK(SaveDataMount3(&mount, &other) == OK);
|
|
std::strcpy(param.title, "Other title");
|
|
CHECK(SaveDataSetParam(&other.mount_point, 0, ¶m, sizeof(param)) == OK);
|
|
constexpr char typed_title[] = "Typed title";
|
|
CHECK(SaveDataSetParam(&other.mount_point, 1, typed_title, sizeof(typed_title)) == OK);
|
|
char title[32] {};
|
|
CHECK(SaveDataGetParam(&other.mount_point, 1, title, sizeof(title), &got) == OK);
|
|
CHECK(std::string(title) == typed_title && got == sizeof(typed_title));
|
|
const uint32_t user_param = 77;
|
|
CHECK(SaveDataSetParam(&other.mount_point, 4, &user_param, sizeof(user_param)) == OK);
|
|
CHECK(SaveDataGetParam(&other.mount_point, 0, &loaded, sizeof(loaded), nullptr) == OK);
|
|
CHECK(std::string(loaded.title) == typed_title && loaded.user_param == user_param);
|
|
CHECK(SaveDataGetParam(&other.mount_point, 5, &loaded.mtime, sizeof(loaded.mtime), nullptr) == OK);
|
|
CHECK(loaded.mtime > 0);
|
|
CHECK(SaveDataSetParam(&other.mount_point, 5, &loaded.mtime, sizeof(loaded.mtime)) ==
|
|
SAVE_DATA_ERROR_PARAMETER);
|
|
CHECK(SaveDataGetParam(&transferred.mount_point, 0, &loaded, sizeof(loaded), nullptr) == OK);
|
|
CHECK(std::string(loaded.title) == "Updated save");
|
|
CHECK(SaveDataUmount2(0, &other.mount_point) == OK);
|
|
CHECK(SaveDataUmount2(0, &transferred.mount_point) == OK);
|
|
|
|
std::array<SceSaveDataDirName, 1> names {};
|
|
std::array<SaveDataParam, 1> params {};
|
|
SaveDataDirNameSearchCond cond {};
|
|
cond.user_id = 1;
|
|
cond.title_id = &source_title;
|
|
SaveDataDirNameSearchResult result {};
|
|
result.dir_names = names.data();
|
|
result.dir_names_num = names.size();
|
|
result.params = params.data();
|
|
CHECK(SaveDataDirNameSearch(&cond, &result) == OK && result.set_num == 1);
|
|
CHECK(std::string(names[0].data) == "slot" && std::string(params[0].title) == "Updated save");
|
|
CHECK(params[0].mtime == nested_mtime);
|
|
}
|
|
|
|
void RunChild(const fs::path& executable, const char* mode) {
|
|
#ifdef _WIN32
|
|
CHECK(_spawnl(_P_WAIT, executable.string().c_str(), executable.string().c_str(), mode,
|
|
static_cast<char*>(nullptr)) == 0);
|
|
#else
|
|
const pid_t pid = fork();
|
|
CHECK(pid >= 0);
|
|
if (pid == 0) {
|
|
execl(executable.c_str(), executable.c_str(), mode, static_cast<char*>(nullptr));
|
|
_exit(127);
|
|
}
|
|
int status = 0;
|
|
CHECK(waitpid(pid, &status, 0) == pid);
|
|
CHECK(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
|
#endif
|
|
}
|
|
} // namespace
|
|
|
|
int main(int argc, char** argv) {
|
|
if (argc == 2) {
|
|
if (std::string_view(argv[1]) == "write") {
|
|
WriteRestartFixture();
|
|
} else {
|
|
CHECK(std::string_view(argv[1]) == "read");
|
|
ReadRestartFixture();
|
|
}
|
|
return 0;
|
|
}
|
|
const auto executable = fs::absolute(argv[0]);
|
|
const auto previous = fs::current_path();
|
|
const auto unique = std::chrono::steady_clock::now().time_since_epoch().count();
|
|
const auto temp = fs::temp_directory_path() / ("kyty_save_memory_" + std::to_string(unique));
|
|
CHECK(fs::create_directories(temp));
|
|
fs::current_path(temp);
|
|
RunChild(executable, "write");
|
|
RunChild(executable, "read");
|
|
TestValidationAndScatter();
|
|
TestIsolationAndSync();
|
|
TestFailedWritePreservesSave();
|
|
TestClassicSavePaths();
|
|
TestSaveAllocations();
|
|
TestClassicSaveParams();
|
|
CHECK(SaveDataTerminate() == OK);
|
|
fs::current_path(previous);
|
|
fs::remove_all(temp);
|
|
return 0;
|
|
}
|