Persist SaveDataMemory2 save data to disk (fixes Astro Bot progress not saving) (#659)

* Persist save-data memory across restarts

* Fix SetSaveDataMemory2 data loss, split saves per user, add atomic writes everywhere

- SaveDataSetSaveDataMemory2 treated data_num=0 as "no entries" even when
  a valid data pointer was supplied. Astro Bot calls this with data_num=0
  meaning "one entry, at data[0]" (an implicit-count convention), so every
  real save write from it was silently dropped as a no-op. Default
  data_num to 1 when data is non-null, matching that convention.
- SaveDataSetupSaveDataMemory2/Get/SetSaveDataMemory2 rejected any call
  that included a non-null icon pointer. Icon persistence isn't
  implemented, but titles are allowed to pass one anyway; rejecting the
  whole call over it made every title that does so fail outright. Ignore
  icon instead of rejecting.
- SaveDataMount3/SaveDataTransferringMount/SaveDataDelete built their
  save-data path from title+dir_name only, dropping the user_id PS5
  local users are keyed by (unlike the newer SetSaveDataMemory2 layout,
  which already nests by user_id). Nest these under user_id too so saves
  from different local users can no longer collide. No migration for the
  old flat layout - the project is still pre-release/experimental.
- Classic SaveDataMount3-mounted titles (Blasphemous, Dead Cells, ...)
  write their save files directly through the guest's normal file I/O
  once mounted, bypassing SaveData's own write path entirely, so they
  had none of the crash-safety SetSaveDataMemory2 already had. Added the
  same protection at the file-system layer: a guest file opened under a
  /savedataN mount that recreates the file from scratch (O_CREAT and/or
  O_TRUNC) is now written to a .tmp shadow file and atomically replaces
  the real one on close, instead of being truncated in place.
- Both write paths now back up the file they're about to replace into a
  sibling "backups" folder (same filename, no added suffix) before the
  atomic replace, so a manual recovery copy of the previous save always
  survives a write - even one that "succeeds" but wasn't wanted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Fix save-memory compatibility and preserve existing saves

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Çağatay Güçlü
2026-09-19 23:51:22 +03:00
committed by GitHub
parent f100f785da
commit cfbaee07f0
4 changed files with 773 additions and 53 deletions
+7
View File
@@ -450,6 +450,12 @@ add_executable(audio_out2_port_tests EXCLUDE_FROM_ALL
target_link_libraries(audio_out2_port_tests common fmt::fmt)
target_include_directories(audio_out2_port_tests PRIVATE ${inc_headers})
add_executable(save_data_memory_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/SaveDataMemoryTests.cpp"
)
target_link_libraries(save_data_memory_tests common fmt::fmt)
target_include_directories(save_data_memory_tests PRIVATE ${inc_headers})
add_executable(event_queue_lifetime_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/EventQueueLifetimeTests.cpp"
"${KYTY_SOURCE_DIR}/kernel/eventQueue.cpp"
@@ -534,6 +540,7 @@ if(BUILD_TESTING)
add_test(NAME kernel_file_system COMMAND $<TARGET_FILE:kernel_file_system_tests>)
add_test(NAME sync_on_address COMMAND $<TARGET_FILE:sync_on_address_tests>)
add_test(NAME audio_out2_port COMMAND $<TARGET_FILE:audio_out2_port_tests>)
add_test(NAME save_data_memory COMMAND $<TARGET_FILE:save_data_memory_tests>)
add_test(NAME shader_recompiler_compute COMMAND $<TARGET_FILE:shader_recompiler_compute_tests>)
add_test(NAME shader_recompiler_alignbyte
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --alignbyte-only)
+306 -53
View File
@@ -14,7 +14,9 @@
#include <algorithm>
#include <cstring>
#include <ctime>
#include <deque>
#include <map>
#include <vector>
namespace Libs {
@@ -176,6 +178,13 @@ struct SaveDataMemorySet2 {
uint8_t reserved[24];
};
struct SaveDataMemorySync {
int32_t user_id;
uint32_t slot_id;
uint32_t option;
uint8_t reserved[28];
};
struct SaveDataTransferringMount {
int32_t user_id;
const SceSaveDataTitleId* title_id;
@@ -230,23 +239,155 @@ static constexpr uint32_t SAVE_DATA_COMMIT_MODE_BACKUP_ASYNC = 1u;
static constexpr uint32_t SAVE_DATA_EVENT_TYPE_UMOUNT_BACKUP_END = 1u;
static constexpr uint32_t SAVE_DATA_EVENT_TYPE_BACKUP_END = 2u;
static constexpr uint32_t SAVE_DATA_EVENT_TYPE_MEMORY_SYNC_END = 3u;
static constexpr uint32_t SAVE_DATA_EVENT_TYPE_COMMIT_BACKUP_END = 4u;
static std::vector<uint8_t> g_save_data_memory(0x10000);
static constexpr size_t SAVE_DATA_MEMORY_MAX_SIZE = 32 * 1024 * 1024;
static constexpr uint32_t SAVE_DATA_MEMORY_SET_PARAM = 1u;
static constexpr uint32_t SAVE_DATA_MEMORY_DOUBLE_BUFFER = 2u;
struct SaveDataMemory {
std::vector<uint8_t> data;
SaveDataParam param {};
uint32_t option = 0;
};
static std::map<std::filesystem::path, SaveDataMemory> g_save_data_memory;
static int32_t g_next_transaction_resource = 1;
static std::deque<SaveDataEvent> g_save_data_events;
static SaveDataMountSlots g_mount_slots;
static Common::Mutex g_mount_mutex;
static bool valid_path_component(std::string_view name) {
return !name.empty() && name != "." && name != ".." &&
name.find_first_not_of(
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-.@") ==
std::string_view::npos;
}
template <size_t N>
static bool valid_path_component(const char (&name)[N]) {
const auto end = std::find(name, name + N, '\0');
return end != name + N && valid_path_component(std::string_view(name, end - name));
}
static std::string get_title_id() {
std::string title_id;
if (!Loader::SystemContentParamSfoGetString("TITLE_ID", &title_id) || title_id.empty()) {
if (!Loader::SystemContentParamSfoGetString("TITLE_ID", &title_id) ||
!valid_path_component(title_id)) {
title_id = "UNKNOWN";
}
return title_id;
}
static std::string memory_dir_name(uint32_t slot_id) {
return "sce_sdmemory" + (slot_id == 0 ? std::string() : std::to_string(slot_id));
}
static std::filesystem::path save_directory(std::string_view title_id, std::string_view dir_name,
int32_t user_id) {
const auto directory = std::filesystem::path(SAVE_DATA_DIR) / title_id / dir_name;
for (uint32_t slot = 0; slot < 4; slot++) {
if (dir_name == memory_dir_name(slot)) {
return directory / std::to_string(user_id);
}
}
return directory;
}
static std::filesystem::path memory_directory(int32_t user_id, uint32_t slot_id) {
return save_directory(get_title_id(), memory_dir_name(slot_id), user_id);
}
static bool valid_memory_range(const SaveDataMemoryData& data, size_t size) {
return data.buf != nullptr && data.offset >= 0 && static_cast<uint64_t>(data.offset) <= size &&
data.buf_size <= size - static_cast<size_t>(data.offset);
}
// Keep memory.dat as raw guest bytes. Replacing a flushed temporary file protects the
// previous save if writing fails or the emulator exits partway through an update.
static int write_memory_file(const std::filesystem::path& path, const void* data, uint32_t size) {
const auto temporary = std::filesystem::path(path.string() + ".tmp");
Common::File file;
if (!file.Create(temporary)) {
return SAVE_DATA_ERROR_INTERNAL;
}
uint32_t written = 0;
file.Write(data, size, &written);
const bool flushed = file.Flush();
file.Close();
std::error_code error;
if (written == size && flushed) {
// Common::File::RenameFile deliberately refuses to replace an existing file.
std::filesystem::rename(temporary, path, error);
if (!error) {
return OK;
}
}
std::filesystem::remove(temporary, error);
return SAVE_DATA_ERROR_INTERNAL;
}
static int save_memory(const std::filesystem::path& directory, const SaveDataMemory& memory) {
std::error_code error;
std::filesystem::create_directories(directory, error);
if (error) {
return SAVE_DATA_ERROR_INTERNAL;
}
// Display metadata is a separate host sidecar, leaving memory.dat as raw guest bytes.
int result = write_memory_file(directory / "param.bin", &memory.param, sizeof(memory.param));
if (result == OK) {
result =
write_memory_file(directory / "memory.dat", memory.data.data(), memory.data.size());
}
return result;
}
static int load_memory(const std::filesystem::path& directory, SaveDataMemory* memory,
size_t* existed_size) {
std::error_code error;
const bool exists = std::filesystem::exists(directory / "memory.dat", error);
if (error) {
return SAVE_DATA_ERROR_INTERNAL;
}
if (!exists) {
return OK;
}
Common::File file(directory / "memory.dat", Common::File::Mode::Read);
if (file.IsInvalid()) {
return SAVE_DATA_ERROR_INTERNAL;
}
*existed_size = file.Size();
if (*existed_size == 0 || *existed_size > SAVE_DATA_MEMORY_MAX_SIZE) {
return SAVE_DATA_ERROR_BROKEN;
}
const auto size = static_cast<uint32_t>(std::min(*existed_size, memory->data.size()));
uint32_t read = 0;
file.Read(memory->data.data(), size, &read);
if (read != size) {
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;
}
static void queue_save_data_event(uint32_t type, int32_t user_id,
const SceSaveDataTitleId* title_id,
const SceSaveDataDirName* dir_name, int32_t error_code = OK) {
@@ -322,6 +463,7 @@ int KYTY_SYSV_ABI SaveDataTerminate() {
return SAVE_DATA_ERROR_BUSY;
}
g_save_data_events.clear();
g_save_data_memory.clear();
return OK;
}
@@ -347,10 +489,17 @@ int KYTY_SYSV_ABI SaveDataDirNameSearch(const SaveDataDirNameSearchCond* cond,
PRINT_NAME();
if (cond == nullptr || result == nullptr ||
(cond->title_id != nullptr && !valid_path_component(cond->title_id->data)) ||
(result->dir_names_num != 0 && result->dir_names == nullptr) ||
(cond->dir_name != nullptr &&
std::memchr(cond->dir_name->data, 0, sizeof(cond->dir_name->data)) == nullptr) ||
static_cast<uint32_t>(cond->key) > static_cast<uint32_t>(SaveDataSortKey::FreeBlocks) ||
static_cast<uint32_t>(cond->order) > static_cast<uint32_t>(SaveDataSortOrder::Descent)) {
return SAVE_DATA_ERROR_PARAMETER;
}
if (cond->user_id < 0) {
return SAVE_DATA_ERROR_INVALID_LOGIN_USER;
}
LOGF("\t user_id = %d\n"
"\t title_id = %s\n"
@@ -369,14 +518,15 @@ int KYTY_SYSV_ABI SaveDataDirNameSearch(const SaveDataDirNameSearchCond* cond,
std::memset(result->reserved, 0, sizeof(result->reserved));
result->pad2 = 0;
Common::LockGuard lock(g_mount_mutex);
const std::string title_id = cond->title_id != nullptr ? cond->title_id->data : get_title_id();
const auto root = std::filesystem::path(SAVE_DATA_DIR) / title_id;
std::vector<std::string> dir_list;
std::string root =
Common::FixDirectorySlash((std::string(SAVE_DATA_DIR) + "/" + get_title_id()));
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_")) {
!entry.name.starts_with("sce_") && valid_path_component(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())) {
@@ -423,9 +573,13 @@ int KYTY_SYSV_ABI SaveDataDirNameSearch(const SaveDataDirNameSearchCond* cond,
int KYTY_SYSV_ABI SaveDataMount3(const SaveDataMount3* mount, SaveDataMountResult* mount_result) {
PRINT_NAME();
EXIT_NOT_IMPLEMENTED(mount == nullptr);
EXIT_NOT_IMPLEMENTED(mount_result == nullptr);
EXIT_NOT_IMPLEMENTED(mount->dir_name == nullptr);
if (mount == nullptr || mount_result == nullptr || mount->dir_name == nullptr ||
!valid_path_component(mount->dir_name->data)) {
return SAVE_DATA_ERROR_PARAMETER;
}
if (mount->user_id < 0) {
return SAVE_DATA_ERROR_INVALID_LOGIN_USER;
}
LOGF("\t user_id = %d\n"
"\t dir_name = %s\n"
@@ -439,9 +593,8 @@ int KYTY_SYSV_ABI SaveDataMount3(const SaveDataMount3* mount, SaveDataMountResul
*mount_result = {};
Common::LockGuard lock(g_mount_mutex);
const std::string dir_name = mount->dir_name->data;
const std::string mount_dir =
std::string(SAVE_DATA_DIR) + "/" + get_title_id() + "/" + dir_name;
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 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));
@@ -481,9 +634,13 @@ int KYTY_SYSV_ABI SaveDataSetupSaveDataMemory2(const SaveDataMemorySetup2* setup
SaveDataMemorySetupResult* result) {
PRINT_NAME();
if (setup_param == nullptr) {
if (setup_param == nullptr || setup_param->slot_id >= 4 || setup_param->memory_size == 0 ||
setup_param->memory_size > SAVE_DATA_MEMORY_MAX_SIZE || (setup_param->option & ~3u) != 0) {
return SAVE_DATA_ERROR_PARAMETER;
}
if (setup_param->user_id < 0) {
return SAVE_DATA_ERROR_INVALID_LOGIN_USER;
}
LOGF("\t option = 0x%08" PRIx32 "\n"
"\t user_id = %" PRId32 "\n"
@@ -493,13 +650,41 @@ int KYTY_SYSV_ABI SaveDataSetupSaveDataMemory2(const SaveDataMemorySetup2* setup
setup_param->option, setup_param->user_id, static_cast<uint64_t>(setup_param->memory_size),
static_cast<uint64_t>(setup_param->icon_memory_size), setup_param->slot_id);
if (setup_param->memory_size > g_save_data_memory.size()) {
g_save_data_memory.resize(setup_param->memory_size);
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)) ==
SaveDataMountSlots::BUSY) {
return SAVE_DATA_ERROR_BUSY;
}
size_t total = setup_param->memory_size *
((setup_param->option & SAVE_DATA_MEMORY_DOUBLE_BUFFER) != 0 ? 2 : 1);
for (const auto& [path, memory]: g_save_data_memory) {
total +=
memory.data.size() * ((memory.option & SAVE_DATA_MEMORY_DOUBLE_BUFFER) != 0 ? 2 : 1);
}
if (g_save_data_memory.size() >= 4 || total > SAVE_DATA_MEMORY_MAX_SIZE) {
return SAVE_DATA_ERROR_LIMITATION_OVER;
}
SaveDataMemory memory;
memory.data.resize(setup_param->memory_size);
memory.option = setup_param->option;
std::snprintf(memory.param.title, sizeof(memory.param.title), "Saved Data");
size_t existed_size = 0;
const int status = load_memory(directory, &memory, &existed_size);
if (status != OK) {
return status;
}
if (existed_size == 0 && setup_param->init_param != nullptr &&
(memory.option & SAVE_DATA_MEMORY_SET_PARAM) != 0) {
memory.param = *setup_param->init_param;
memory.param.mtime = 0;
}
g_save_data_memory.emplace(directory, std::move(memory));
if (result != nullptr) {
*result = {};
result->existed_memory_size = g_save_data_memory.size();
result->existed_memory_size = existed_size;
}
return OK;
@@ -508,9 +693,12 @@ int KYTY_SYSV_ABI SaveDataSetupSaveDataMemory2(const SaveDataMemorySetup2* setup
int KYTY_SYSV_ABI SaveDataGetSaveDataMemory2(SaveDataMemoryGet2* get_param) {
PRINT_NAME();
if (get_param == nullptr) {
if (get_param == nullptr || get_param->slot_id >= 4) {
return SAVE_DATA_ERROR_PARAMETER;
}
if (get_param->user_id < 0) {
return SAVE_DATA_ERROR_INVALID_LOGIN_USER;
}
LOGF("\t user_id = %" PRId32 "\n"
"\t data = 0x%016" PRIx64 "\n"
@@ -521,20 +709,23 @@ int KYTY_SYSV_ABI SaveDataGetSaveDataMemory2(SaveDataMemoryGet2* get_param) {
reinterpret_cast<uint64_t>(get_param->param), reinterpret_cast<uint64_t>(get_param->icon),
get_param->slot_id);
Common::LockGuard lock(g_mount_mutex);
const auto it =
g_save_data_memory.find(memory_directory(get_param->user_id, get_param->slot_id));
if (it == g_save_data_memory.end()) {
return SAVE_DATA_ERROR_MEMORY_NOT_READY;
}
const auto& memory = it->second;
if ((get_param->param != nullptr && (memory.option & SAVE_DATA_MEMORY_SET_PARAM) == 0) ||
(get_param->data != nullptr && !valid_memory_range(*get_param->data, memory.data.size()))) {
return SAVE_DATA_ERROR_PARAMETER;
}
if (get_param->data != nullptr) {
auto* data = get_param->data;
if (data->buf == nullptr || data->offset < 0) {
return SAVE_DATA_ERROR_PARAMETER;
}
const auto offset = static_cast<size_t>(data->offset);
if (offset + data->buf_size > g_save_data_memory.size()) {
g_save_data_memory.resize(offset + data->buf_size);
}
std::memcpy(data->buf, g_save_data_memory.data() + offset, data->buf_size);
const auto& data = *get_param->data;
std::memcpy(data.buf, memory.data.data() + data.offset, data.buf_size);
}
if (get_param->param != nullptr) {
std::memset(get_param->param, 0, sizeof(*get_param->param));
*get_param->param = memory.param;
}
if (get_param->icon != nullptr) {
get_param->icon->data_size = 0;
@@ -546,9 +737,13 @@ int KYTY_SYSV_ABI SaveDataGetSaveDataMemory2(SaveDataMemoryGet2* get_param) {
int KYTY_SYSV_ABI SaveDataSetSaveDataMemory2(const SaveDataMemorySet2* set_param) {
PRINT_NAME();
if (set_param == nullptr) {
if (set_param == nullptr || set_param->slot_id >= 4 || set_param->data_num > 5 ||
(set_param->data == nullptr && set_param->data_num != 0)) {
return SAVE_DATA_ERROR_PARAMETER;
}
if (set_param->user_id < 0) {
return SAVE_DATA_ERROR_INVALID_LOGIN_USER;
}
LOGF("\t user_id = %" PRId32 "\n"
"\t data = 0x%016" PRIx64 "\n"
@@ -560,23 +755,42 @@ int KYTY_SYSV_ABI SaveDataSetSaveDataMemory2(const SaveDataMemorySet2* set_param
reinterpret_cast<uint64_t>(set_param->param), reinterpret_cast<uint64_t>(set_param->icon),
set_param->data_num, set_param->slot_id);
const uint32_t data_num = (set_param->data_num == 0 ? 1 : set_param->data_num);
if (set_param->data != nullptr) {
for (uint32_t i = 0; i < data_num; i++) {
const auto& data = set_param->data[i];
if (data.buf == nullptr || data.offset < 0) {
return SAVE_DATA_ERROR_PARAMETER;
}
const auto offset = static_cast<size_t>(data.offset);
if (offset + data.buf_size > g_save_data_memory.size()) {
g_save_data_memory.resize(offset + data.buf_size);
}
std::memcpy(g_save_data_memory.data() + offset, data.buf, data.buf_size);
Common::LockGuard lock(g_mount_mutex);
const auto it =
g_save_data_memory.find(memory_directory(set_param->user_id, set_param->slot_id));
if (it == g_save_data_memory.end()) {
return SAVE_DATA_ERROR_MEMORY_NOT_READY;
}
if (set_param->param != nullptr && (it->second.option & SAVE_DATA_MEMORY_SET_PARAM) == 0) {
return SAVE_DATA_ERROR_PARAMETER;
}
// Some games supply one descriptor with an implicit zero count.
const uint32_t data_num =
set_param->data != nullptr && set_param->data_num == 0 ? 1 : set_param->data_num;
for (uint32_t i = 0; i < data_num; i++) {
if (!valid_memory_range(set_param->data[i], it->second.data.size())) {
return SAVE_DATA_ERROR_PARAMETER;
}
}
if (data_num == 0 && set_param->param == nullptr) {
return OK;
}
return OK;
// Save the complete update before publishing it so progress survives process exit.
auto updated = it->second;
for (uint32_t i = 0; i < data_num; i++) {
const auto& data = set_param->data[i];
std::memcpy(updated.data.data() + data.offset, data.buf, data.buf_size);
}
if (set_param->param != nullptr) {
updated.param = *set_param->param;
}
updated.param.mtime = static_cast<int64_t>(std::time(nullptr));
const int status = save_memory(it->first, updated);
if (status == OK) {
it->second = std::move(updated);
}
return status;
}
int KYTY_SYSV_ABI SaveDataTransferringMount(const SaveDataTransferringMount* mount,
@@ -584,9 +798,13 @@ int KYTY_SYSV_ABI SaveDataTransferringMount(const SaveDataTransferringMount* mou
PRINT_NAME();
if (mount == nullptr || mount_result == nullptr || mount->title_id == nullptr ||
mount->dir_name == nullptr) {
mount->dir_name == nullptr || !valid_path_component(mount->title_id->data) ||
!valid_path_component(mount->dir_name->data)) {
return SAVE_DATA_ERROR_PARAMETER;
}
if (mount->user_id < 0) {
return SAVE_DATA_ERROR_INVALID_LOGIN_USER;
}
LOGF("\t user_id = %" PRId32 "\n"
"\t title_id = %s\n"
@@ -597,8 +815,7 @@ int KYTY_SYSV_ABI SaveDataTransferringMount(const SaveDataTransferringMount* mou
Common::LockGuard lock(g_mount_mutex);
const std::string dir_name = mount->dir_name->data;
const std::string mount_dir =
std::string(SAVE_DATA_DIR) + "/" + mount->title_id->data + "/" + dir_name;
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);
if (slot == SaveDataMountSlots::BUSY) {
return SAVE_DATA_ERROR_BUSY;
@@ -669,6 +886,7 @@ int KYTY_SYSV_ABI SaveDataCommit(const SaveDataCommitParam* param) {
param->resource, param->commit_mode);
if ((param->commit_mode & SAVE_DATA_COMMIT_MODE_BACKUP_ASYNC) != 0) {
Common::LockGuard lock(g_mount_mutex);
queue_save_data_event(SAVE_DATA_EVENT_TYPE_COMMIT_BACKUP_END, 0, nullptr, nullptr);
}
@@ -678,9 +896,13 @@ int KYTY_SYSV_ABI SaveDataCommit(const SaveDataCommitParam* param) {
int KYTY_SYSV_ABI SaveDataDelete(const SaveDataDelete* del) {
PRINT_NAME();
if (del == nullptr || del->dir_name == nullptr) {
if (del == nullptr || del->dir_name == nullptr || !valid_path_component(del->dir_name->data) ||
(del->title_id != nullptr && !valid_path_component(del->title_id->data))) {
return SAVE_DATA_ERROR_PARAMETER;
}
if (del->user_id < 0) {
return SAVE_DATA_ERROR_INVALID_LOGIN_USER;
}
LOGF("\t user_id = %" PRId32 "\n"
"\t title_id = %s\n"
@@ -688,13 +910,17 @@ int KYTY_SYSV_ABI SaveDataDelete(const SaveDataDelete* del) {
del->user_id, del->title_id != nullptr ? del->title_id->data : "<default>",
del->dir_name->data);
std::string dir =
std::string(SAVE_DATA_DIR) + "/" + get_title_id() + "/" + std::string(del->dir_name->data);
if (Common::File::IsDirectoryExisting(dir)) {
Common::File::DeleteDirectory(dir);
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 ||
g_save_data_memory.contains(dir)) {
return SAVE_DATA_ERROR_BUSY;
}
std::error_code error;
std::filesystem::remove_all(dir, error);
return OK;
return error ? SAVE_DATA_ERROR_INTERNAL : OK;
}
int KYTY_SYSV_ABI SaveDataGetParam(const SaveDataMountPoint* mount_point, uint32_t param_type,
@@ -760,10 +986,35 @@ int KYTY_SYSV_ABI SaveDataSaveIconByPath(const SaveDataMountPoint* mount_point,
return OK;
}
int KYTY_SYSV_ABI SaveDataSyncSaveDataMemory(const void* sync_param) {
int KYTY_SYSV_ABI SaveDataSyncSaveDataMemory(const SaveDataMemorySync* sync_param) {
PRINT_NAME();
LOGF("\t sync_param = 0x%016" PRIx64 "\n", reinterpret_cast<uint64_t>(sync_param));
if (sync_param == nullptr || sync_param->slot_id >= 4 || sync_param->option > 1) {
return SAVE_DATA_ERROR_PARAMETER;
}
if (sync_param->user_id < 0) {
return SAVE_DATA_ERROR_INVALID_LOGIN_USER;
}
Common::LockGuard lock(g_mount_mutex);
const auto it =
g_save_data_memory.find(memory_directory(sync_param->user_id, sync_param->slot_id));
if (it == g_save_data_memory.end()) {
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) {
return SAVE_DATA_ERROR_BUSY;
}
const int status = save_memory(it->first, it->second);
if (sync_param->option == 1) {
return status;
}
SceSaveDataTitleId title {};
SceSaveDataDirName directory {};
std::snprintf(title.data, sizeof(title.data), "%s", get_title_id().c_str());
std::snprintf(directory.data, sizeof(directory.data), "%s", dir_name.c_str());
queue_save_data_event(SAVE_DATA_EVENT_TYPE_MEMORY_SYNC_END, sync_param->user_id, &title,
&directory, status);
return OK;
}
@@ -779,6 +1030,7 @@ int KYTY_SYSV_ABI SaveDataGetEventResult(const void* event_param, SaveDataEvent*
return SAVE_DATA_ERROR_PARAMETER;
}
Common::LockGuard lock(g_mount_mutex);
if (g_save_data_events.empty()) {
return SAVE_DATA_ERROR_NOT_FOUND;
}
@@ -812,6 +1064,7 @@ int KYTY_SYSV_ABI SaveDataBackup(const SaveDataBackup* backup) {
backup->user_id, backup->title_id != nullptr ? backup->title_id->data : "<default>",
backup->dir_name->data);
Common::LockGuard lock(g_mount_mutex);
queue_save_data_event(SAVE_DATA_EVENT_TYPE_BACKUP_END, backup->user_id, backup->title_id,
backup->dir_name);
+28
View File
@@ -89,6 +89,33 @@ void CheckSaveRename(const std::filesystem::path &root,
"renamed save contents");
}
void TestSaveOpenVisibility() {
constexpr char Path[] = "/savedata0/visible-save.dat";
constexpr char Payload[] = "saved progress";
const int fd = FileSystem::KernelOpen(Path, 0xa01, 0777);
Check(fd >= 3, "create save file exclusively");
FileSystem::FileStat stat {};
Check(FileSystem::KernelStat(Path, &stat) == OK && stat.st_size == 0,
"created save file is visible before close");
Check(FileSystem::KernelOpen(Path, 0xa01, 0777) ==
Libs::LibKernel::KERNEL_ERROR_EEXIST,
"exclusive creation detects an open save file");
Check(FileSystem::KernelWrite(fd, Payload, sizeof(Payload) - 1) ==
sizeof(Payload) - 1,
"populate save file before truncation");
Check(FileSystem::KernelClose(fd) == OK, "close populated save file");
Check(FileSystem::KernelStat(Path, &stat) == OK &&
stat.st_size == sizeof(Payload) - 1,
"save file contains the truncation fixture");
const int truncated = FileSystem::KernelOpen(Path, 0x401, 0777);
Check(truncated >= 3, "truncate existing save file");
Check(FileSystem::KernelStat(Path, &stat) == OK && stat.st_size == 0,
"save truncation is visible before close");
Check(FileSystem::KernelClose(truncated) == OK, "close truncated save file");
}
void CheckMountRoot(const std::filesystem::path &root) {
Common::File cache;
Check(cache.Create(root / "rpf.cache"), "create directory listing fixture");
@@ -390,6 +417,7 @@ int main() {
CheckDirectoryStream(temporary.Path());
CheckAprPaths(temporary.Path());
FileSystem::Mount(temporary.Path(), "/savedata0");
TestSaveOpenVisibility();
CheckSaveRename(temporary.Path(), "first-save");
CheckSaveRename(temporary.Path(), "replacement-save");
FileSystem::Shutdown();
+432
View File
@@ -0,0 +1,432 @@
// 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;
} // 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 {
Common::Time GetTime() {
return Common::Time(0);
}
} // namespace Timer
} // namespace Loader
namespace Libs::LibKernel::FileSystem {
void Mount(const std::filesystem::path& directory, const std::string&) {
g_mounted_directory = directory;
}
void Umount(const std::string&) {}
} // 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 = &param;
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 = &param;
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 = &param;
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"};
fs::create_directories("_SaveData/CLASSIC/save.1");
{
std::ofstream saved("_SaveData/CLASSIC/save.1/progress");
saved << "save.1";
}
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;
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 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();
CHECK(SaveDataTerminate() == OK);
fs::current_path(previous);
fs::remove_all(temp);
return 0;
}