Fix save metadata and timestamps

Co-authored-by: Ali Almohaya <AliAlmohaya@gmail.com>
Co-authored-by: CheesyPoofs346 <jjs777406@gmail.com>
This commit is contained in:
nmzik
2026-09-23 22:47:34 +04:00
parent 7d7267cc56
commit 569ec04d1a
3 changed files with 315 additions and 65 deletions
+171 -55
View File
@@ -122,6 +122,20 @@ struct SaveDataParam {
uint8_t reserved[32];
};
struct SaveParamTextField {
char* data;
size_t size;
};
static SaveParamTextField param_text_field(SaveDataParam& param, uint32_t type) {
switch (type) {
case 1: return {param.title, sizeof(param.title)};
case 2: return {param.sub_title, sizeof(param.sub_title)};
case 3: return {param.detail, sizeof(param.detail)};
default: return {nullptr, 0};
}
}
struct SaveDataMountInfo {
uint64_t blocks;
uint64_t free_blocks;
@@ -345,6 +359,62 @@ static int read_save_blocks(const std::filesystem::path& directory, uint64_t* bl
return OK;
}
static int read_save_param(const std::filesystem::path& path, SaveDataParam* param) {
std::error_code error;
const bool exists = std::filesystem::exists(path, error);
if (error) {
return SAVE_DATA_ERROR_INTERNAL;
}
if (!exists) {
return SAVE_DATA_ERROR_NOT_FOUND;
}
Common::File file(path, Common::File::Mode::Read);
if (file.IsInvalid()) {
return SAVE_DATA_ERROR_INTERNAL;
}
if (file.Size() != sizeof(*param)) {
return SAVE_DATA_ERROR_BROKEN;
}
uint32_t read = 0;
file.Read(param, sizeof(*param), &read);
return read == sizeof(*param) ? OK : SAVE_DATA_ERROR_BROKEN;
}
static std::filesystem::path save_param_path(const std::filesystem::path& directory) {
return directory / "sce_sys" / "param.bin";
}
static int64_t newest_save_time(const std::filesystem::path& directory) {
int64_t mtime = 0;
std::error_code error;
for (std::filesystem::recursive_directory_iterator it(
directory, std::filesystem::directory_options::skip_permission_denied, error),
end;
it != end; it.increment(error)) {
if (error) {
error.clear();
continue;
}
if (it->is_regular_file(error) && !error) {
const auto time = Common::File::GetLastWriteTimeUTC(it->path());
if (!time.IsInvalid()) {
mtime = std::max(mtime, static_cast<int64_t>(time.ToUnix()));
}
}
error.clear();
}
return mtime;
}
static int load_save_param(const std::filesystem::path& directory, SaveDataParam* param) {
*param = {};
const int status = read_save_param(save_param_path(directory), param);
if (status == OK) {
param->mtime = newest_save_time(directory);
}
return status;
}
static int save_memory(const std::filesystem::path& directory, const SaveDataMemory& memory) {
std::error_code error;
std::filesystem::create_directories(directory, error);
@@ -385,23 +455,7 @@ static int load_memory(const std::filesystem::path& directory, SaveDataMemory* m
return SAVE_DATA_ERROR_BROKEN;
}
file.Close();
const bool has_param = std::filesystem::exists(directory / "param.bin", error);
if (error) {
return SAVE_DATA_ERROR_INTERNAL;
}
if (has_param) {
if (!file.Open(directory / "param.bin", Common::File::Mode::Read)) {
return SAVE_DATA_ERROR_INTERNAL;
}
if (file.Size() != sizeof(memory->param)) {
return SAVE_DATA_ERROR_BROKEN;
}
file.Read(&memory->param, sizeof(memory->param), &read);
if (read != sizeof(memory->param)) {
return SAVE_DATA_ERROR_BROKEN;
}
}
return OK;
return read_save_param(directory / "param.bin", &memory->param);
}
static void queue_save_data_event(uint32_t type, int32_t user_id,
@@ -451,11 +505,11 @@ static bool dir_name_match(const char* str, const char* pattern) {
return *str == '\0' && *pattern == '\0';
}
static int mount_save_data(int slot, std::string_view dir_name, const std::string& directory,
static int mount_save_data(int slot, const std::filesystem::path& directory,
uint32_t status, SaveDataMountResult* result) {
const std::string mount_point = SaveDataMountSlots::MountPoint(static_cast<size_t>(slot));
LibKernel::FileSystem::Mount(directory, mount_point);
g_mount_slots.Mount(static_cast<size_t>(slot), dir_name);
g_mount_slots.Mount(static_cast<size_t>(slot), directory);
std::snprintf(result->mount_point.data, sizeof(result->mount_point.data), "%s",
mount_point.c_str());
result->required_blocks = 0;
@@ -542,7 +596,8 @@ int KYTY_SYSV_ABI SaveDataDirNameSearch(const SaveDataDirNameSearchCond* cond,
if (Common::File::IsDirectoryExisting(root)) {
for (const auto& entry: Common::File::GetDirEntries(root)) {
if (!entry.is_file && entry.name != "." && entry.name != ".." &&
!entry.name.starts_with("sce_") && valid_path_component(entry.name)) {
!entry.name.starts_with("sce_") && valid_path_component(entry.name) &&
Common::File::IsFileExisting(save_param_path(root / entry.name))) {
if (cond->dir_name == nullptr || cond->dir_name->data[0] == '\0' ||
dir_name_match(Common::ToLower(entry.name).c_str(),
Common::ToLower(std::string(cond->dir_name->data)).c_str())) {
@@ -574,7 +629,10 @@ int KYTY_SYSV_ABI SaveDataDirNameSearch(const SaveDataDirNameSearchCond* cond,
std::snprintf(result->dir_names[i].data, sizeof(result->dir_names[i].data), "%s",
dir_list[i].c_str());
if (result->params != nullptr) {
result->params[i] = {};
const int status = load_save_param(root / dir_list[i], &result->params[i]);
if (status != OK) {
return status;
}
}
if (result->infos != nullptr) {
auto& info = result->infos[i];
@@ -615,12 +673,12 @@ int KYTY_SYSV_ABI SaveDataMount3(const SaveDataMount3* mount, SaveDataMountResul
Common::LockGuard lock(g_mount_mutex);
const std::string dir_name = mount->dir_name->data;
const auto mount_dir = save_directory(get_title_id(), dir_name, mount->user_id).string();
const auto mount_dir = save_directory(get_title_id(), dir_name, mount->user_id);
const bool create = ((mount->mount_mode & 4u) != 0);
const bool create2 = ((mount->mount_mode & 32u) != 0);
const bool open = (!create && !create2 && ((mount->mount_mode & 3u) != 0));
const int slot = g_mount_slots.FindAvailable(dir_name);
const int slot = g_mount_slots.FindAvailable(mount_dir);
if (slot == SaveDataMountSlots::BUSY) {
return SAVE_DATA_ERROR_BUSY;
}
@@ -645,7 +703,7 @@ int KYTY_SYSV_ABI SaveDataMount3(const SaveDataMount3* mount, SaveDataMountResul
if (mount->blocks < SAVE_DATA_BLOCKS_MIN || mount->blocks > SAVE_DATA_BLOCKS_MAX) {
return SAVE_DATA_ERROR_PARAMETER;
}
const auto metadata = std::filesystem::path(mount_dir) / "sce_sys";
const auto metadata = mount_dir / "sce_sys";
std::error_code error;
std::filesystem::create_directories(metadata, error);
if (error) {
@@ -657,10 +715,17 @@ int KYTY_SYSV_ABI SaveDataMount3(const SaveDataMount3* mount, SaveDataMountResul
std::filesystem::remove_all(mount_dir, error);
return status;
}
const SaveDataParam initial {};
const int param_status = write_save_file(save_param_path(mount_dir), &initial,
sizeof(initial));
if (param_status != OK) {
std::filesystem::remove_all(mount_dir, error);
return param_status;
}
created = true;
}
return mount_save_data(slot, dir_name, mount_dir, created ? 1u : 0u, mount_result);
return mount_save_data(slot, mount_dir, created ? 1u : 0u, mount_result);
}
int KYTY_SYSV_ABI SaveDataSetupSaveDataMemory2(const SaveDataMemorySetup2* setup_param,
@@ -686,7 +751,7 @@ int KYTY_SYSV_ABI SaveDataSetupSaveDataMemory2(const SaveDataMemorySetup2* setup
Common::LockGuard lock(g_mount_mutex);
const auto directory = memory_directory(setup_param->user_id, setup_param->slot_id);
if (g_save_data_memory.contains(directory) ||
g_mount_slots.FindAvailable(memory_dir_name(setup_param->slot_id)) ==
g_mount_slots.FindAvailable(directory) ==
SaveDataMountSlots::BUSY) {
return SAVE_DATA_ERROR_BUSY;
}
@@ -848,8 +913,8 @@ int KYTY_SYSV_ABI SaveDataTransferringMount(const SaveDataTransferringMount* mou
Common::LockGuard lock(g_mount_mutex);
const std::string dir_name = mount->dir_name->data;
const auto mount_dir = save_directory(mount->title_id->data, dir_name, mount->user_id).string();
const int slot = g_mount_slots.FindAvailable(dir_name);
const auto mount_dir = save_directory(mount->title_id->data, dir_name, mount->user_id);
const int slot = g_mount_slots.FindAvailable(mount_dir);
if (slot == SaveDataMountSlots::BUSY) {
return SAVE_DATA_ERROR_BUSY;
}
@@ -861,7 +926,7 @@ int KYTY_SYSV_ABI SaveDataTransferringMount(const SaveDataTransferringMount* mou
return SAVE_DATA_ERROR_NOT_FOUND;
}
return mount_save_data(slot, dir_name, mount_dir, 0, mount_result);
return mount_save_data(slot, mount_dir, 0, mount_result);
}
int KYTY_SYSV_ABI SaveDataUmount2(uint32_t mode, const SaveDataMountPoint* mount_point) {
@@ -946,7 +1011,7 @@ int KYTY_SYSV_ABI SaveDataDelete(const SaveDataDelete* del) {
Common::LockGuard lock(g_mount_mutex);
const auto dir = save_directory(del->title_id != nullptr ? del->title_id->data : get_title_id(),
del->dir_name->data, del->user_id);
if (g_mount_slots.FindAvailable(del->dir_name->data) == SaveDataMountSlots::BUSY ||
if (g_mount_slots.FindAvailable(dir) == SaveDataMountSlots::BUSY ||
g_save_data_memory.contains(dir)) {
return SAVE_DATA_ERROR_BUSY;
}
@@ -960,7 +1025,8 @@ int KYTY_SYSV_ABI SaveDataGetParam(const SaveDataMountPoint* mount_point, uint32
void* param_buf, size_t param_buf_size, size_t* got_size) {
PRINT_NAME();
if (mount_point == nullptr || param_buf == nullptr) {
if (mount_point == nullptr || param_buf == nullptr || param_type > 5 ||
std::memchr(mount_point->data, 0, sizeof(mount_point->data)) == nullptr) {
return SAVE_DATA_ERROR_PARAMETER;
}
@@ -968,21 +1034,44 @@ int KYTY_SYSV_ABI SaveDataGetParam(const SaveDataMountPoint* mount_point, uint32
"\t param_type = %u\n"
"\t param_buf_size = %" PRIu64 "\n",
mount_point->data, param_type, param_buf_size);
if (param_type == 0) {
if (param_buf_size < sizeof(SaveDataParam)) {
return SAVE_DATA_ERROR_PARAMETER;
}
std::memset(param_buf, 0, sizeof(SaveDataParam));
if (got_size != nullptr) {
*got_size = sizeof(SaveDataParam);
}
return OK;
Common::LockGuard lock(g_mount_mutex);
const int slot = g_mount_slots.Find(mount_point->data);
if (slot == SaveDataMountSlots::FULL) {
return SAVE_DATA_ERROR_NOT_MOUNTED;
}
std::memset(param_buf, 0, param_buf_size);
SaveDataParam param {};
const int status = load_save_param(g_mount_slots.Directory(static_cast<size_t>(slot)), &param);
if (status != OK) {
return status;
}
size_t size = 0;
if (param_type == 0) {
size = sizeof(param);
if (param_buf_size < size) {
return SAVE_DATA_ERROR_PARAMETER;
}
std::memcpy(param_buf, &param, size);
} else if (param_type == 4 || param_type == 5) {
size = param_type == 4 ? sizeof(param.user_param) : sizeof(param.mtime);
if (param_buf_size < size) {
return SAVE_DATA_ERROR_PARAMETER;
}
std::memcpy(param_buf, param_type == 4 ? static_cast<const void*>(&param.user_param)
: static_cast<const void*>(&param.mtime), size);
} else {
if (param_buf_size == 0) {
return SAVE_DATA_ERROR_PARAMETER;
}
const auto field = param_text_field(param, param_type);
size = std::min(static_cast<size_t>(
std::find(field.data, field.data + field.size, '\0') - field.data),
param_buf_size - 1);
std::memcpy(param_buf, field.data, size);
static_cast<char*>(param_buf)[size++] = '\0';
}
if (got_size != nullptr) {
*got_size = param_buf_size;
*got_size = size;
}
return OK;
@@ -1035,7 +1124,7 @@ int KYTY_SYSV_ABI SaveDataSyncSaveDataMemory(const SaveDataMemorySync* sync_para
return SAVE_DATA_ERROR_MEMORY_NOT_READY;
}
const auto dir_name = memory_dir_name(sync_param->slot_id);
if (g_mount_slots.FindAvailable(dir_name) == SaveDataMountSlots::BUSY) {
if (g_mount_slots.FindAvailable(it->first) == SaveDataMountSlots::BUSY) {
return SAVE_DATA_ERROR_BUSY;
}
const int status = save_memory(it->first, it->second);
@@ -1108,26 +1197,53 @@ int KYTY_SYSV_ABI SaveDataSetParam(const SaveDataMountPoint* mount_point, uint32
const void* param_buf, size_t param_buf_size) {
PRINT_NAME();
EXIT_NOT_IMPLEMENTED(mount_point == nullptr);
if (mount_point == nullptr || param_buf == nullptr || param_type > 4 ||
std::memchr(mount_point->data, 0, sizeof(mount_point->data)) == nullptr ||
(param_type == 0 && param_buf_size < sizeof(SaveDataParam)) ||
(param_type == 4 && param_buf_size < sizeof(uint32_t)) ||
(param_type >= 1 && param_type <= 3 && param_buf_size == 0)) {
return SAVE_DATA_ERROR_PARAMETER;
}
LOGF("\t mount_point = %s\n"
"\t param_type = %u\n"
"\t param_buf_size = %" PRIu64 "\n",
mount_point->data, param_type, param_buf_size);
Common::LockGuard lock(g_mount_mutex);
const int slot = g_mount_slots.Find(mount_point->data);
if (slot == SaveDataMountSlots::FULL) {
return SAVE_DATA_ERROR_NOT_MOUNTED;
}
if (param_type == 0) {
const auto* p = static_cast<const SaveDataParam*>(param_buf);
LOGF("\t title = %s\n"
"\t sub_title = %s\n"
"\t detail = %s\n"
LOGF("\t title = %.128s\n"
"\t sub_title = %.128s\n"
"\t detail = %.1024s\n"
"\t user_param = %u\n",
p->title, p->sub_title, p->detail, p->user_param);
} else {
LOGF("\t unsupported param_type, accepting as no-op\n");
return write_save_file(save_param_path(g_mount_slots.Directory(static_cast<size_t>(slot))),
p, sizeof(*p));
}
return OK;
SaveDataParam param {};
const auto path = save_param_path(g_mount_slots.Directory(static_cast<size_t>(slot)));
const int status = read_save_param(path, &param);
if (status != OK) {
return status;
}
if (param_type == 4) {
std::memcpy(&param.user_param, param_buf, sizeof(param.user_param));
} else {
const auto field = param_text_field(param, param_type);
const auto* text = static_cast<const char*>(param_buf);
const size_t length = std::min(param_buf_size, field.size - 1);
const auto* end = static_cast<const char*>(std::memchr(text, '\0', length));
const size_t count = end != nullptr ? static_cast<size_t>(end - text) : length;
std::memset(field.data, 0, field.size);
std::memcpy(field.data, text, count);
}
return write_save_file(path, &param, sizeof(param));
}
int KYTY_SYSV_ABI SaveDataGetMountInfo(const SaveDataMountPoint* mount_point,
@@ -1139,12 +1255,12 @@ int KYTY_SYSV_ABI SaveDataGetMountInfo(const SaveDataMountPoint* mount_point,
return SAVE_DATA_ERROR_PARAMETER;
}
Common::LockGuard lock(g_mount_mutex);
if (g_mount_slots.Find(mount_point->data) == SaveDataMountSlots::FULL) {
const int slot = g_mount_slots.Find(mount_point->data);
if (slot == SaveDataMountSlots::FULL) {
return SAVE_DATA_ERROR_NOT_MOUNTED;
}
uint64_t blocks = 0;
const int status = read_save_blocks(
LibKernel::FileSystem::GetRealFilename(mount_point->data), &blocks);
const int status = read_save_blocks(g_mount_slots.Directory(static_cast<size_t>(slot)), &blocks);
if (status != OK) {
return status;
}
+16 -5
View File
@@ -2,9 +2,11 @@
#define EMULATOR_INCLUDE_EMULATOR_LIBS_SAVEDATAMOUNTSLOTS_H_
#include <array>
#include <filesystem>
#include <optional>
#include <string>
#include <string_view>
#include <system_error>
namespace Libs::SaveData {
@@ -14,12 +16,15 @@ public:
static constexpr int FULL = -1;
static constexpr size_t COUNT = 16;
[[nodiscard]] int FindAvailable(std::string_view directory) const {
[[nodiscard]] int FindAvailable(const std::filesystem::path& directory) const {
int available = FULL;
for (size_t index = 0; index < m_directories.size(); index++) {
const auto& mounted = m_directories[index];
if (mounted == directory) {
return BUSY;
if (mounted.has_value()) {
std::error_code error;
if (*mounted == directory || std::filesystem::equivalent(*mounted, directory, error)) {
return BUSY;
}
}
if (!mounted.has_value() && available == FULL) {
available = static_cast<int>(index);
@@ -28,7 +33,13 @@ public:
return available;
}
void Mount(size_t slot, std::string_view directory) { m_directories[slot] = directory; }
void Mount(size_t slot, const std::filesystem::path& directory) {
m_directories[slot] = directory;
}
[[nodiscard]] const std::filesystem::path& Directory(size_t slot) const {
return *m_directories[slot];
}
void Release(size_t slot) {
if (slot < m_directories.size()) {
@@ -59,7 +70,7 @@ public:
}
private:
std::array<std::optional<std::string>, COUNT> m_directories;
std::array<std::optional<std::filesystem::path>, COUNT> m_directories;
};
} // namespace Libs::SaveData
+128 -5
View File
@@ -330,11 +330,16 @@ std::vector<std::string> Search(int32_t user, const SceSaveDataTitleId* title =
void TestClassicSavePaths() {
Reset("CLASSIC");
const std::vector<std::string> names {"save.1", "save1", "slot@A", "slotA"};
fs::create_directories("_SaveData/CLASSIC/save.1");
{
std::ofstream saved("_SaveData/CLASSIC/save.1/progress");
saved << "save.1";
}
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());
@@ -455,6 +460,123 @@ void TestSaveAllocations() {
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, &param, sizeof(param) - 1) ==
SAVE_DATA_ERROR_PARAMETER);
CHECK(SaveDataSetParam(&mounted.mount_point, 0, &param, 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, &param, 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, &param, 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, &param, 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,
@@ -496,6 +618,7 @@ int main(int argc, char** argv) {
TestFailedWritePreservesSave();
TestClassicSavePaths();
TestSaveAllocations();
TestClassicSaveParams();
CHECK(SaveDataTerminate() == OK);
fs::current_path(previous);
fs::remove_all(temp);