#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "SaveData.hpp" #include "prx/libSceSaveData/SaveDataFile.hpp" static constexpr char SAVE_DIR[] = "_sd"; static std::atomic g_transaction_counter{1}; static std::atomic g_initializations{0}; static std::string save_root() { return std::string(SAVE_DIR); } // Events the title polls with sceSaveDataGetEventResult; a backup completes at once here. static std::mutex g_state_mutex; static std::deque g_events; constexpr std::uint32_t SAVE_DATA_EVENT_TYPE_BACKUP = 2; static bool dir_name_match(const char* str, const char* pattern) { if (pattern == nullptr || pattern[0] == '\0') { return true; } while (*str != '\0' && *pattern != '\0') { if (*pattern == '%') { for (const char* s = str;; s++) { if (dir_name_match(s, pattern + 1)) { return true; } if (*s == '\0') { break; } } return false; } if (*pattern == '_') { str++; pattern++; continue; } if (*pattern != *str) { return false; } str++; pattern++; } return *str == '\0' && *pattern == '\0'; } namespace { // Save-data memory: one blob per user and slot, kept next to the save directory (_sd_mem/u/slot.bin), // with a small .param sidecar holding the last SaveDataParam the title wrote. Keying by user as well as slot // keeps two users' in-memory saves from colliding on the same console. constexpr char MEM_DIR[] = "_sd_mem"; constexpr std::size_t MEM_MAX_SIZE = 0x1000000; // 16 MiB std::mutex g_mem_mutex; std::string mem_path(std::int32_t user_id, std::uint32_t slot, const char* ext) { char buf[96]; std::snprintf(buf, sizeof(buf), "%s/u%d/slot%u.%s", MEM_DIR, static_cast(user_id), static_cast(slot), ext); return buf; } bool file_size_of(const std::string& path, std::size_t* out) { std::error_code ec; if (!std::filesystem::is_regular_file(path, ec)) { return false; } const auto sz = std::filesystem::file_size(path, ec); if (ec) { return false; } *out = static_cast(sz); return true; } // Atomic-ish write: write to a temp file then rename over the target, so a kill mid-write never // leaves a torn save behind. bool write_file_replace(const std::string& path, const std::vector& data) { return savedata::replace_file(path, data.data(), data.size()); } bool read_file_all(const std::string& path, std::vector& out) { std::ifstream f(path, std::ios::binary); if (!f) { return false; } f.seekg(0, std::ios::end); const auto n = static_cast(f.tellg()); f.seekg(0); out.resize(n); if (n != 0) { f.read(out.data(), static_cast(n)); } return static_cast(f); } std::string param_path(const std::string& real_path) { return real_path + ".param"; } bool read_param_file(const std::string& path, SaveDataParam* param) { if (!std::filesystem::exists(path)) { return false; } std::vector bytes; if (!read_file_all(path, bytes)) { throw std::runtime_error("SaveData: cannot read param file " + path); } if (bytes.size() != sizeof(SaveDataParam)) { throw std::runtime_error("SaveData: param file " + path + " is " + std::to_string(bytes.size()) + " bytes, expected " + std::to_string(sizeof(SaveDataParam)) + " (outdated format)"); } std::memcpy(param, bytes.data(), sizeof(SaveDataParam)); return true; } SaveDataParam load_param(const std::string& real_path) { SaveDataParam param{}; read_param_file(param_path(real_path), ¶m); if (param.mtime == 0) { std::error_code ec; const auto written = std::filesystem::last_write_time(real_path, ec); if (!ec) { const auto system = std::chrono::clock_cast(written); param.mtime = std::chrono::duration_cast(system.time_since_epoch()).count(); } } return param; } bool store_param(const std::string& real_path, const SaveDataParam& param) { std::vector bytes(sizeof(param)); std::memcpy(bytes.data(), ¶m, sizeof(param)); return write_file_replace(param_path(real_path), bytes); } struct ParamField { std::size_t offset; std::size_t size; bool text; }; bool param_field(std::uint32_t param_type, ParamField* field) { switch (param_type) { case SAVE_DATA_PARAM_TYPE_ALL: *field = {0, sizeof(SaveDataParam), false}; return true; case SAVE_DATA_PARAM_TYPE_TITLE: *field = {offsetof(SaveDataParam, title), sizeof(SaveDataParam::title), true}; return true; case SAVE_DATA_PARAM_TYPE_SUB_TITLE: *field = {offsetof(SaveDataParam, sub_title), sizeof(SaveDataParam::sub_title), true}; return true; case SAVE_DATA_PARAM_TYPE_DETAIL: *field = {offsetof(SaveDataParam, detail), sizeof(SaveDataParam::detail), true}; return true; case SAVE_DATA_PARAM_TYPE_USER_PARAM: *field = {offsetof(SaveDataParam, user_param), sizeof(SaveDataParam::user_param), false}; return true; case SAVE_DATA_PARAM_TYPE_MTIME: *field = {offsetof(SaveDataParam, mtime), sizeof(SaveDataParam::mtime), false}; return true; default: return false; } } int traceLimit() { static const int limit = std::getenv("APS5_SAVEDATA_TRACE") != nullptr ? std::numeric_limits::max() : 3; return limit; } #define SAVEDATA_TRACE(...) \ do { \ static std::atomic traceCount{0}; \ if (traceCount.fetch_add(1, std::memory_order_relaxed) < traceLimit()) { \ std::fprintf(stderr, "[SAVEDATA:native] " __VA_ARGS__); \ std::fputc('\n', stderr); \ std::fflush(stderr); \ } \ } while (0) } // namespace extern "C" { int APS5_VABI sceSaveDataBackup(const SaveDataBackup* backup) { if (backup == nullptr || backup->dir_name == nullptr) { throw std::runtime_error("sceSaveDataBackup: null argument"); } // The system copies the save directory to its backup area asynchronously; the copy itself is // not observable by the title, only its completion event. SaveDataEvent event{}; event.type = SAVE_DATA_EVENT_TYPE_BACKUP; event.error_code = SAVE_DATA_OK; event.user_id = backup->user_id; if (backup->title_id != nullptr) event.title_id = *backup->title_id; event.dir_name = *backup->dir_name; std::lock_guard lock(g_state_mutex); g_events.push_back(event); return SAVE_DATA_OK; } int APS5_VABI sceSaveDataCommit(const SaveDataCommitParam* param) { (void)param; return SAVE_DATA_OK; } int APS5_VABI sceSaveDataCreateTransactionResource(uint32_t size) { (void)size; return g_transaction_counter.fetch_add(1); } static int deleteSave(const SaveDataDelete* del) { if (del == nullptr || del->dir_name == nullptr) { throw std::runtime_error("sceSaveDataDelete: null argument"); } const auto* nameEnd = static_cast(std::memchr(del->dir_name->data, '\0', sizeof(del->dir_name->data))); if (nameEnd == nullptr) { return SAVE_DATA_ERROR_PARAMETER; } const std::string dirName(del->dir_name->data, static_cast(nameEnd - del->dir_name->data)); if (dirName.empty() || dirName == "." || dirName == ".." || dirName.find_first_of("/\\:") != std::string::npos) { return SAVE_DATA_ERROR_PARAMETER; } const std::string path = save_root() + "/" + dirName; std::lock_guard lock(g_slots_mutex); for (const auto& slot : g_slots) { if (!slot.used) { continue; } if (slot.real_path == path) { return SAVE_DATA_ERROR_BUSY; } std::error_code ec; const bool same_directory = std::filesystem::equivalent(slot.real_path, path, ec); if (ec && ec != std::errc::no_such_file_or_directory) { throw std::filesystem::filesystem_error("sceSaveDataDelete: failed to compare save paths", slot.real_path, path, ec); } if (same_directory) { return SAVE_DATA_ERROR_BUSY; } } if (std::filesystem::is_directory(path)) { std::filesystem::remove_all(path); } std::error_code ec; std::filesystem::remove(param_path(path), ec); return SAVE_DATA_OK; } int APS5_VABI sceSaveDataDelete(const SaveDataDelete* del) { const int rc = deleteSave(del); SAVEDATA_TRACE("delete dir=%s -> 0x%08x", del != nullptr && del->dir_name != nullptr ? del->dir_name->data : "(null)", static_cast(rc)); return rc; } int APS5_VABI sceSaveDataDeleteTransactionResource(int32_t resource) { (void)resource; return SAVE_DATA_OK; } static int dirNameSearch(const SaveDataDirNameSearchCond* cond, SaveDataDirNameSearchResult* result) { if (cond == nullptr || result == nullptr) { throw std::runtime_error("sceSaveDataDirNameSearch: null argument"); } result->hit_num = 0; result->set_num = 0; const char* pattern = (cond->dir_name != nullptr) ? cond->dir_name->data : nullptr; const std::string root = save_root(); if (!std::filesystem::is_directory(root)) { return SAVE_DATA_OK; } std::uint32_t hit = 0; std::uint32_t set = 0; for (const auto& entry : std::filesystem::directory_iterator(root)) { if (!entry.is_directory()) { continue; } const std::string name = entry.path().filename().string(); if (!dir_name_match(name.c_str(), pattern)) { continue; } hit++; if (result->dir_names != nullptr && set < result->dir_names_num) { std::snprintf(result->dir_names[set].data, sizeof(result->dir_names[set].data), "%s", name.c_str()); if (result->params != nullptr) { result->params[set] = load_param(entry.path().string()); } set++; } } result->hit_num = hit; result->set_num = set; return SAVE_DATA_OK; } int APS5_VABI sceSaveDataDirNameSearch(const SaveDataDirNameSearchCond* cond, SaveDataDirNameSearchResult* result) { const int rc = dirNameSearch(cond, result); SAVEDATA_TRACE("dirNameSearch user=%d pattern=%s hit=%u set=%u -> 0x%08x", cond != nullptr ? cond->user_id : -1, cond != nullptr && cond->dir_name != nullptr ? cond->dir_name->data : "(all)", result != nullptr ? result->hit_num : 0u, result != nullptr ? result->set_num : 0u, static_cast(rc)); return rc; } int APS5_VABI sceSaveDataGetEventResult(const void* event_param, SaveDataEvent* event) { (void)event_param; if (event == nullptr) { throw std::runtime_error("sceSaveDataGetEventResult: null event"); } std::lock_guard lock(g_state_mutex); if (g_events.empty()) { return SAVE_DATA_ERROR_NOT_FOUND; } *event = g_events.front(); g_events.pop_front(); return SAVE_DATA_OK; } static int getMountInfo(const SaveDataMountPoint* mount_point, SaveDataMountInfo* info) { if (mount_point == nullptr || info == nullptr) { throw std::runtime_error("sceSaveDataGetMountInfo: null argument"); } std::lock_guard lock(g_slots_mutex); if (find_slot_by_mount_point(mount_point->data) == -1) { return SAVE_DATA_ERROR_NOT_MOUNTED; } std::memset(info, 0, sizeof(*info)); info->blocks = SAVE_DATA_BLOCKS_MAX; info->free_blocks = SAVE_DATA_BLOCKS_MAX; return SAVE_DATA_OK; } int APS5_VABI sceSaveDataGetMountInfo(const SaveDataMountPoint* mount_point, SaveDataMountInfo* info) { const int rc = getMountInfo(mount_point, info); SAVEDATA_TRACE("getMountInfo point=%s -> 0x%08x", mount_point != nullptr ? mount_point->data : "(null)", static_cast(rc)); return rc; } static int getParam(const SaveDataMountPoint* mount_point, uint32_t param_type, void* param_buf, size_t param_buf_size, size_t* got_size) { if (mount_point == nullptr || param_buf == nullptr) { throw std::runtime_error("sceSaveDataGetParam: null argument"); } std::lock_guard lock(g_slots_mutex); const int slot = find_slot_by_mount_point(mount_point->data); if (slot == -1) { return SAVE_DATA_ERROR_NOT_MOUNTED; } ParamField field{}; if (!param_field(param_type, &field) || param_buf_size < field.size) { return SAVE_DATA_ERROR_PARAMETER; } const SaveDataParam param = load_param(g_slots[slot].real_path); std::memcpy(param_buf, reinterpret_cast(¶m) + field.offset, field.size); if (got_size != nullptr) { *got_size = field.size; } return SAVE_DATA_OK; } int APS5_VABI sceSaveDataGetParam(const SaveDataMountPoint* mount_point, uint32_t param_type, void* param_buf, size_t param_buf_size, size_t* got_size) { const int rc = getParam(mount_point, param_type, param_buf, param_buf_size, got_size); SAVEDATA_TRACE("getParam point=%s type=%u size=%zu -> 0x%08x got=%zu", mount_point != nullptr ? mount_point->data : "(null)", param_type, param_buf_size, static_cast(rc), got_size != nullptr ? *got_size : static_cast(0)); return rc; } static int getSaveDataMemory2(SaveDataMemoryGet2* get_param) { if (get_param == nullptr) { return SAVE_DATA_ERROR_PARAMETER; } if (g_initializations == 0) { return SAVE_DATA_ERROR_NOT_INITIALIZED; } std::lock_guard lk(g_mem_mutex); const std::string path = mem_path(get_param->user_id, get_param->slot_id, "bin"); std::size_t size = 0; if (!file_size_of(path, &size)) { return SAVE_DATA_ERROR_MEMORY_NOT_READY; } const SaveDataMemoryData* d = get_param->data; if (d != nullptr && d->buf_size != 0) { if (d->buf == nullptr || d->offset > size || d->buf_size > size - d->offset) { return SAVE_DATA_ERROR_PARAMETER; } std::ifstream f(path, std::ios::binary); if (!f) { return SAVE_DATA_ERROR_INTERNAL; } f.seekg(static_cast(d->offset)); f.read(static_cast(d->buf), static_cast(d->buf_size)); if (!f) { return SAVE_DATA_ERROR_INTERNAL; } } if (get_param->param != nullptr) { std::memset(get_param->param, 0, sizeof(SaveDataParam)); read_param_file(mem_path(get_param->user_id, get_param->slot_id, "param"), get_param->param); } if (get_param->icon != nullptr) { get_param->icon->data_size = 0; } return SAVE_DATA_OK; } int APS5_VABI sceSaveDataGetSaveDataMemory2(SaveDataMemoryGet2* get_param) { const int rc = getSaveDataMemory2(get_param); SAVEDATA_TRACE("getMemory2 user=%d slot=%u -> 0x%08x", get_param != nullptr ? get_param->user_id : -1, get_param != nullptr ? get_param->slot_id : 0u, static_cast(rc)); return rc; } int APS5_VABI sceSaveDataInitialize3(const void* init) { (void)init; ++g_initializations; return SAVE_DATA_OK; } int APS5_VABI sceSaveDataLoadIcon(const SaveDataMountPoint* mount_point, SaveDataIcon* icon) { (void)icon; if (mount_point == nullptr) { throw std::runtime_error("sceSaveDataLoadIcon: null mount_point"); } std::lock_guard lock(g_slots_mutex); if (find_slot_by_mount_point(mount_point->data) == -1) { return SAVE_DATA_ERROR_NOT_MOUNTED; } if (icon != nullptr) { icon->data_size = 0; } return SAVE_DATA_OK; } static int mount3(const SaveDataMount3* mount, SaveDataMountResult* mount_result) { if (mount == nullptr || mount_result == nullptr || mount->dir_name == nullptr) { throw std::runtime_error("sceSaveDataMount3: null argument"); } std::memset(mount_result, 0, sizeof(*mount_result)); const bool create = (mount->mount_mode & SAVE_DATA_MOUNT_MODE_CREATE) != 0; const bool create2 = (mount->mount_mode & SAVE_DATA_MOUNT_MODE_CREATE2) != 0; const bool rdonly = (mount->mount_mode & SAVE_DATA_MOUNT_MODE_RDONLY) != 0; const bool rdwr = (mount->mount_mode & SAVE_DATA_MOUNT_MODE_RDWR) != 0; const bool open = !create && !create2 && (rdonly || rdwr); if (!create && !create2 && !open) { throw std::runtime_error("sceSaveDataMount3: unknown mount_mode"); } const auto* nameEnd = static_cast(std::memchr(mount->dir_name->data, '\0', sizeof(mount->dir_name->data))); if (nameEnd == nullptr) { throw std::runtime_error("sceSaveDataMount3: unterminated directory name"); } const std::string dirName(mount->dir_name->data, static_cast(nameEnd - mount->dir_name->data)); if (dirName.empty() || dirName == "." || dirName == ".." || dirName.find_first_of("/\\:") != std::string::npos) { throw std::runtime_error("sceSaveDataMount3: invalid directory name"); } const std::string real_path = save_root() + "/" + dirName; std::lock_guard lock(g_slots_mutex); for (const auto& used : g_slots) { if (used.used && used.real_path == real_path) { return SAVE_DATA_ERROR_BUSY; } } const bool exists = std::filesystem::is_directory(real_path); if (create && exists) { return SAVE_DATA_ERROR_EXISTS; } if (open && !exists) { return SAVE_DATA_ERROR_NOT_FOUND; } int slot = find_free_slot(); if (slot == -1) { return SAVE_DATA_ERROR_MOUNT_FULL; } if (create || create2) { std::filesystem::create_directories(real_path); } // The title gets a short mount point (16 bytes on the PS5) and opens files under it; the // path resolver maps it to the save directory, whose name may be far longer. const std::string mountPoint = "/savedata" + std::to_string(slot); AddPathAlias_nid_no_patch(mountPoint.c_str(), std::filesystem::absolute(real_path).string().c_str()); g_slots[slot].used = true; g_slots[slot].mount_point = mountPoint; g_slots[slot].real_path = real_path; std::memcpy(mount_result->mount_point.data, mountPoint.c_str(), mountPoint.size() + 1); mount_result->required_blocks = 0; mount_result->mount_status = (create || create2) ? 1u : 0u; return SAVE_DATA_OK; } int APS5_VABI sceSaveDataMount3(const SaveDataMount3* mount, SaveDataMountResult* mount_result) { const int rc = mount3(mount, mount_result); SAVEDATA_TRACE("mount3 user=%d dir=%s mode=0x%x blocks=%llu -> 0x%08x point=%s status=%u", mount != nullptr ? mount->user_id : -1, mount != nullptr && mount->dir_name != nullptr ? mount->dir_name->data : "(null)", mount != nullptr ? mount->mount_mode : 0u, mount != nullptr ? static_cast(mount->blocks) : 0ull, static_cast(rc), mount_result != nullptr ? mount_result->mount_point.data : "", mount_result != nullptr ? mount_result->mount_status : 0u); return rc; } int APS5_VABI sceSaveDataPrepare(const SaveDataMountPoint* mount_point, const SaveDataPrepareParam* param) { (void)mount_point; (void)param; return SAVE_DATA_OK; } int APS5_VABI sceSaveDataSaveIcon(const SaveDataMountPoint* mount_point, const SaveDataIcon* icon) { (void)icon; if (mount_point == nullptr) { throw std::runtime_error("sceSaveDataSaveIcon: null mount_point"); } std::lock_guard lock(g_slots_mutex); if (find_slot_by_mount_point(mount_point->data) == -1) { return SAVE_DATA_ERROR_NOT_MOUNTED; } return SAVE_DATA_OK; } int APS5_VABI sceSaveDataSaveIconByPath(const SaveDataMountPoint* mount_point, const char* path) { (void)mount_point; (void)path; return SAVE_DATA_OK; } static int setParam(const SaveDataMountPoint* mount_point, uint32_t param_type, const void* param_buf, size_t param_buf_size) { if (mount_point == nullptr || param_buf == nullptr) { throw std::runtime_error("sceSaveDataSetParam: null argument"); } std::lock_guard lock(g_slots_mutex); const int slot = find_slot_by_mount_point(mount_point->data); if (slot == -1) { return SAVE_DATA_ERROR_NOT_MOUNTED; } ParamField field{}; if (!param_field(param_type, &field) || param_buf_size == 0 || (!field.text && param_buf_size < field.size)) { return SAVE_DATA_ERROR_PARAMETER; } SaveDataParam param = load_param(g_slots[slot].real_path); char* destination = reinterpret_cast(¶m) + field.offset; if (field.text) { const std::size_t length = std::min(param_buf_size, field.size - 1); const char* text = static_cast(param_buf); const char* end = static_cast(std::memchr(text, '\0', length)); std::memset(destination, 0, field.size); std::memcpy(destination, text, end != nullptr ? static_cast(end - text) : length); } else { std::memcpy(destination, param_buf, field.size); } if (param_type != SAVE_DATA_PARAM_TYPE_MTIME) { param.mtime = static_cast(std::time(nullptr)); } return store_param(g_slots[slot].real_path, param) ? SAVE_DATA_OK : SAVE_DATA_ERROR_INTERNAL; } int APS5_VABI sceSaveDataSetParam(const SaveDataMountPoint* mount_point, uint32_t param_type, const void* param_buf, size_t param_buf_size) { const int rc = setParam(mount_point, param_type, param_buf, param_buf_size); SAVEDATA_TRACE("setParam point=%s type=%u size=%zu -> 0x%08x", mount_point != nullptr ? mount_point->data : "(null)", param_type, param_buf_size, static_cast(rc)); return rc; } static int setSaveDataMemory2(const SaveDataMemorySet2* set_param) { if (set_param == nullptr) { return SAVE_DATA_ERROR_PARAMETER; } if (g_initializations == 0) { return SAVE_DATA_ERROR_NOT_INITIALIZED; } std::lock_guard lk(g_mem_mutex); const std::string path = mem_path(set_param->user_id, set_param->slot_id, "bin"); std::size_t size = 0; if (!file_size_of(path, &size)) { return SAVE_DATA_ERROR_MEMORY_NOT_READY; } // Validate every range first so a bad entry never leaves a partial write. const std::uint32_t n = set_param->data != nullptr ? (set_param->data_num != 0 ? set_param->data_num : 1u) : 0u; for (std::uint32_t i = 0; i < n; i++) { const SaveDataMemoryData& d = set_param->data[i]; if (d.buf_size == 0) { continue; } if (d.buf == nullptr || d.offset > size || d.buf_size > size - d.offset) { return SAVE_DATA_ERROR_PARAMETER; } } if (n != 0) { std::fstream f(path, std::ios::binary | std::ios::in | std::ios::out); if (!f) { return SAVE_DATA_ERROR_INTERNAL; } for (std::uint32_t i = 0; i < n; i++) { const SaveDataMemoryData& d = set_param->data[i]; if (d.buf_size == 0) { continue; } f.seekp(static_cast(d.offset)); f.write(static_cast(d.buf), static_cast(d.buf_size)); } f.flush(); if (!f) { return SAVE_DATA_ERROR_INTERNAL; } } if (set_param->param != nullptr) { std::vector pd(sizeof(SaveDataParam)); std::memcpy(pd.data(), set_param->param, sizeof(SaveDataParam)); if (!write_file_replace(mem_path(set_param->user_id, set_param->slot_id, "param"), pd)) { return SAVE_DATA_ERROR_INTERNAL; } } return SAVE_DATA_OK; } int APS5_VABI sceSaveDataSetSaveDataMemory2(const SaveDataMemorySet2* set_param) { const int rc = setSaveDataMemory2(set_param); SAVEDATA_TRACE("setMemory2 user=%d slot=%u num=%u -> 0x%08x", set_param != nullptr ? set_param->user_id : -1, set_param != nullptr ? set_param->slot_id : 0u, set_param != nullptr ? set_param->data_num : 0u, static_cast(rc)); return rc; } static int setupSaveDataMemory2(const SaveDataMemorySetup2* setup_param, SaveDataMemorySetupResult* result) { if (setup_param == nullptr) { return SAVE_DATA_ERROR_PARAMETER; } if (g_initializations == 0) { return SAVE_DATA_ERROR_NOT_INITIALIZED; } if (setup_param->memory_size == 0 || setup_param->memory_size > MEM_MAX_SIZE) { return SAVE_DATA_ERROR_PARAMETER; } std::lock_guard lk(g_mem_mutex); const std::string path = mem_path(setup_param->user_id, setup_param->slot_id, "bin"); std::size_t existed = 0; const bool have = file_size_of(path, &existed); if (!have) { existed = 0; } // First run: create a zero-filled blob and report existed size 0 so the title treats it as a new save. if (!have || existed < setup_param->memory_size) { std::error_code ec; std::filesystem::create_directories(std::filesystem::path(path).parent_path(), ec); std::vector data; if (have && !read_file_all(path, data)) { return SAVE_DATA_ERROR_INTERNAL; } data.resize(setup_param->memory_size, 0); if (!write_file_replace(path, data)) { return SAVE_DATA_ERROR_INTERNAL; } if (setup_param->init_param != nullptr && (setup_param->option & 1u) != 0) { std::vector pd(sizeof(SaveDataParam)); std::memcpy(pd.data(), setup_param->init_param, sizeof(SaveDataParam)); write_file_replace(mem_path(setup_param->user_id, setup_param->slot_id, "param"), pd); } } if (result != nullptr) { std::memset(result, 0, sizeof(*result)); result->existed_memory_size = have ? existed : 0; } return SAVE_DATA_OK; } int APS5_VABI sceSaveDataSetupSaveDataMemory2(const SaveDataMemorySetup2* setup_param, SaveDataMemorySetupResult* result) { const int rc = setupSaveDataMemory2(setup_param, result); SAVEDATA_TRACE("setupMemory2 user=%d slot=%u size=%zu -> 0x%08x existed=%zu", setup_param != nullptr ? setup_param->user_id : -1, setup_param != nullptr ? setup_param->slot_id : 0u, setup_param != nullptr ? setup_param->memory_size : static_cast(0), static_cast(rc), result != nullptr ? result->existed_memory_size : static_cast(0)); return rc; } int APS5_VABI sceSaveDataSyncSaveDataMemory(const void* sync_param) { if (sync_param == nullptr) { return SAVE_DATA_ERROR_PARAMETER; } // Every Set already writes straight through to disk; sync only has to confirm the memory exists. const std::int32_t user_id = *static_cast(sync_param); const std::uint32_t slot_id = reinterpret_cast(sync_param)[1]; std::lock_guard lk(g_mem_mutex); std::size_t size = 0; if (!file_size_of(mem_path(user_id, slot_id, "bin"), &size)) { return SAVE_DATA_ERROR_MEMORY_NOT_READY; } return SAVE_DATA_OK; } int APS5_VABI sceSaveDataTerminate(void) { std::lock_guard lock(g_slots_mutex); if (g_initializations == 0) { return SAVE_DATA_ERROR_NOT_INITIALIZED; } if (g_initializations == 1 && any_slot_used()) { return SAVE_DATA_ERROR_BUSY; } --g_initializations; return SAVE_DATA_OK; } static int transferringMount(const SaveDataTransferringMount* mount, SaveDataMountResult* mount_result) { (void)mount; if (mount_result != nullptr) { std::memset(mount_result, 0, sizeof(*mount_result)); } // No PS4-to-PS5 transfer data exists on this console. return SAVE_DATA_ERROR_NOT_FOUND; } int APS5_VABI sceSaveDataTransferringMount(const SaveDataTransferringMount* mount, SaveDataMountResult* mount_result) { const int rc = transferringMount(mount, mount_result); SAVEDATA_TRACE("transferringMount -> 0x%08x", static_cast(rc)); return rc; } static int umount2(uint32_t mode, const SaveDataMountPoint* mount_point) { (void)mode; if (mount_point == nullptr) { throw std::runtime_error("sceSaveDataUmount2: null mount_point"); } std::lock_guard lock(g_slots_mutex); int slot = find_slot_by_mount_point(mount_point->data); if (slot == -1) { return SAVE_DATA_ERROR_NOT_MOUNTED; } RemovePathAlias_nid_no_patch(g_slots[slot].mount_point.c_str()); g_slots[slot] = MountSlot{}; return SAVE_DATA_OK; } int APS5_VABI sceSaveDataUmount2(uint32_t mode, const SaveDataMountPoint* mount_point) { const int rc = umount2(mode, mount_point); SAVEDATA_TRACE("umount2 mode=0x%x point=%s -> 0x%08x", mode, mount_point != nullptr ? mount_point->data : "(null)", static_cast(rc)); return rc; } int APS5_VABI sceSaveDataTransferringMountPs4(const SaveDataTransferringMount* mount, SaveDataMountResult* mount_result) { const int rc = transferringMount(mount, mount_result); SAVEDATA_TRACE("transferringMountPs4 -> 0x%08x", static_cast(rc)); return rc; } static int dirNameSearchPs4(const SaveDataDirNameSearchCond* cond, SaveDataDirNameSearchResult* result) { if (cond == nullptr || result == nullptr) { throw std::runtime_error("sceSaveDataDirNameSearchPs4: null argument"); } result->hit_num = 0; result->set_num = 0; return SAVE_DATA_OK; } int APS5_VABI sceSaveDataDirNameSearchPs4(const SaveDataDirNameSearchCond* cond, SaveDataDirNameSearchResult* result) { const int rc = dirNameSearchPs4(cond, result); SAVEDATA_TRACE("dirNameSearchPs4 user=%d -> 0x%08x", cond->user_id, static_cast(rc)); return rc; } }