diff --git a/src/libs/libSaveData.cpp b/src/libs/libSaveData.cpp index 50f280bbd..21e02c0eb 100644 --- a/src/libs/libSaveData.cpp +++ b/src/libs/libSaveData.cpp @@ -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(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(slot)); LibKernel::FileSystem::Mount(directory, mount_point); - g_mount_slots.Mount(static_cast(slot), dir_name); + g_mount_slots.Mount(static_cast(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(slot)), ¶m); + 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, ¶m, 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(¶m.user_param) + : static_cast(¶m.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( + std::find(field.data, field.data + field.size, '\0') - field.data), + param_buf_size - 1); + std::memcpy(param_buf, field.data, size); + static_cast(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(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(slot))), + p, sizeof(*p)); } - - return OK; + SaveDataParam param {}; + const auto path = save_param_path(g_mount_slots.Directory(static_cast(slot))); + const int status = read_save_param(path, ¶m); + if (status != OK) { + return status; + } + if (param_type == 4) { + std::memcpy(¶m.user_param, param_buf, sizeof(param.user_param)); + } else { + const auto field = param_text_field(param, param_type); + const auto* text = static_cast(param_buf); + const size_t length = std::min(param_buf_size, field.size - 1); + const auto* end = static_cast(std::memchr(text, '\0', length)); + const size_t count = end != nullptr ? static_cast(end - text) : length; + std::memset(field.data, 0, field.size); + std::memcpy(field.data, text, count); + } + return write_save_file(path, ¶m, 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(slot)), &blocks); if (status != OK) { return status; } diff --git a/src/libs/saveDataMountSlots.h b/src/libs/saveDataMountSlots.h index 8be28caec..c02143e5f 100644 --- a/src/libs/saveDataMountSlots.h +++ b/src/libs/saveDataMountSlots.h @@ -2,9 +2,11 @@ #define EMULATOR_INCLUDE_EMULATOR_LIBS_SAVEDATAMOUNTSLOTS_H_ #include +#include #include #include #include +#include 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(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, COUNT> m_directories; + std::array, COUNT> m_directories; }; } // namespace Libs::SaveData diff --git a/tests/SaveDataMemoryTests.cpp b/tests/SaveDataMemoryTests.cpp index 048853be3..aeaf67171 100644 --- a/tests/SaveDataMemoryTests.cpp +++ b/tests/SaveDataMemoryTests.cpp @@ -330,11 +330,16 @@ std::vector Search(int32_t user, const SceSaveDataTitleId* title = void TestClassicSavePaths() { Reset("CLASSIC"); const std::vector 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 {"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, ¶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(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 names {}; + std::array 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);