AnyPS5/core/libs/prx/libSceSaveData.native/Export.cpp

844 lines
34 KiB
C++

#include <algorithm>
#include <atomic>
#include <cstddef>
#include <deque>
#include <fstream>
#include <map>
#include <mutex>
#include <vector>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <limits>
#include <cstring>
#include <ctime>
#include <filesystem>
#include <stdexcept>
#include <string>
#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<std::int32_t> g_transaction_counter{1};
static std::atomic<int> g_initializations{0};
static std::atomic<float> g_convertProgress{0.0f};
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<SaveDataEvent> 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++;
}
while (*pattern == '%') {
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<user>/slot<n>.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 = 32u * 1024u * 1024u;
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<int>(user_id), static_cast<unsigned>(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<std::size_t>(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<char>& data) {
return savedata::replace_file(path, data.data(), data.size());
}
bool read_file_all(const std::string& path, std::vector<char>& out) {
std::ifstream f(path, std::ios::binary);
if (!f) {
return false;
}
f.seekg(0, std::ios::end);
const auto n = static_cast<std::size_t>(f.tellg());
f.seekg(0);
out.resize(n);
if (n != 0) {
f.read(out.data(), static_cast<std::streamsize>(n));
}
return static_cast<bool>(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<char> 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), &param);
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::filesystem::file_time_type::clock::to_sys(written);
param.mtime = std::chrono::duration_cast<std::chrono::seconds>(system.time_since_epoch()).count();
}
}
return param;
}
bool store_param(const std::string& real_path, const SaveDataParam& param) {
std::vector<char> bytes(sizeof(param));
std::memcpy(bytes.data(), &param, 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<int>::max() : 3;
return limit;
}
#define SAVEDATA_TRACE(...) \
do { \
static std::atomic<int> 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<const char*>(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<std::size_t>(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<unsigned>(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 std::uint32_t key = cond->key;
if ((key > SAVE_DATA_SORT_KEY_MTIME && key != SAVE_DATA_SORT_KEY_FREE_BLOCKS) || cond->order > SAVE_DATA_SORT_ORDER_DESCENT) {
return SAVE_DATA_ERROR_PARAMETER;
}
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;
}
struct Match {
std::string name;
std::string path;
SaveDataParam param;
};
const bool sortByParam = key == SAVE_DATA_SORT_KEY_USER_PARAM || key == SAVE_DATA_SORT_KEY_MTIME;
std::vector<Match> matches;
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;
}
Match match{name, entry.path().string(), {}};
if (sortByParam) {
match.param = load_param(match.path);
}
matches.push_back(std::move(match));
}
std::sort(matches.begin(), matches.end(), [](const Match& a, const Match& b) { return a.name < b.name; });
if (key == SAVE_DATA_SORT_KEY_USER_PARAM) {
std::stable_sort(matches.begin(), matches.end(), [](const Match& a, const Match& b) { return a.param.user_param < b.param.user_param; });
} else if (key == SAVE_DATA_SORT_KEY_MTIME) {
std::stable_sort(matches.begin(), matches.end(), [](const Match& a, const Match& b) { return a.param.mtime < b.param.mtime; });
}
if (cond->order == SAVE_DATA_SORT_ORDER_DESCENT) {
std::reverse(matches.begin(), matches.end());
}
const std::uint32_t set = result->dir_names == nullptr ? 0 : static_cast<std::uint32_t>(std::min<std::size_t>(result->dir_names_num, matches.size()));
for (std::uint32_t i = 0; i < set; i++) {
std::snprintf(result->dir_names[i].data, sizeof(result->dir_names[i].data), "%s", matches[i].name.c_str());
if (result->params != nullptr) {
result->params[i] = sortByParam ? matches[i].param : load_param(matches[i].path);
}
if (result->infos != nullptr) {
result->infos[i] = SaveDataSearchInfo{SAVE_DATA_BLOCKS_MAX, SAVE_DATA_BLOCKS_MAX, {}};
}
}
result->hit_num = static_cast<std::uint32_t>(matches.size());
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<unsigned>(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<unsigned>(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<const char*>(&param) + 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<unsigned>(rc), got_size != nullptr ? *got_size : static_cast<size_t>(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<std::mutex> 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<std::streamoff>(d->offset));
f.read(static_cast<char*>(d->buf), static_cast<std::streamsize>(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<unsigned>(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");
}
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<const char*>(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<std::size_t>(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());
std::memset(mount_result, 0, sizeof(*mount_result));
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 && !exists)) ? 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<unsigned long long>(mount->blocks) : 0ull, static_cast<unsigned>(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<char*>(&param) + field.offset;
if (field.text) {
const std::size_t length = std::min(param_buf_size, field.size - 1);
const char* text = static_cast<const char*>(param_buf);
const char* end = static_cast<const char*>(std::memchr(text, '\0', length));
std::memset(destination, 0, field.size);
std::memcpy(destination, text, end != nullptr ? static_cast<std::size_t>(end - text) : length);
} else {
std::memcpy(destination, param_buf, field.size);
}
if (param_type != SAVE_DATA_PARAM_TYPE_MTIME) {
param.mtime = static_cast<std::int64_t>(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<unsigned>(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<std::mutex> 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::vector<char> memory;
if (!read_file_all(path, memory) || memory.size() != size) {
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;
}
std::memcpy(memory.data() + d.offset, d.buf, d.buf_size);
}
if (!write_file_replace(path, memory)) {
return SAVE_DATA_ERROR_INTERNAL;
}
}
if (set_param->param != nullptr) {
std::vector<char> 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<unsigned>(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<std::mutex> 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;
}
const bool needParam = (setup_param->init_param != nullptr && (setup_param->option & 1u) != 0);
const std::string paramPath = mem_path(setup_param->user_id, setup_param->slot_id, "param");
std::size_t paramSize = 0;
const bool haveParam = file_size_of(paramPath, &paramSize);
// 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 || (needParam && !haveParam)) {
std::error_code ec;
std::filesystem::create_directories(std::filesystem::path(path).parent_path(), ec);
if (needParam) {
std::vector<char> pd(sizeof(SaveDataParam));
std::memcpy(pd.data(), setup_param->init_param, sizeof(SaveDataParam));
if (!write_file_replace(paramPath, pd)) {
return SAVE_DATA_ERROR_INTERNAL;
}
}
if (!have || existed < setup_param->memory_size) {
std::vector<char> 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 (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<size_t>(0), static_cast<unsigned>(rc), result != nullptr ? result->existed_memory_size : static_cast<size_t>(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<const std::int32_t*>(sync_param);
const std::uint32_t slot_id = reinterpret_cast<const std::uint32_t*>(sync_param)[1];
std::lock_guard<std::mutex> 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;
if (g_initializations == 0) {
g_convertProgress = 0.0f;
}
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<unsigned>(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<unsigned>(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<unsigned>(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<unsigned>(rc));
return rc;
}
static int convert(const SaveDataConvert* param) {
if (param == nullptr || param->dir_name == nullptr) {
throw std::runtime_error("sceSaveDataConvert: null argument");
}
g_convertProgress = 0.0f;
const auto* nameEnd = static_cast<const char*>(std::memchr(param->dir_name->data, '\0', sizeof(param->dir_name->data)));
if (nameEnd == nullptr) {
return SAVE_DATA_ERROR_PARAMETER;
}
const std::string dirName(param->dir_name->data, static_cast<std::size_t>(nameEnd - param->dir_name->data));
if (dirName.empty() || dirName == "." || dirName == ".." || dirName.find_first_of("/\\:") != std::string::npos) {
return SAVE_DATA_ERROR_PARAMETER;
}
if (!std::filesystem::is_directory(save_root() + "/" + dirName)) {
return SAVE_DATA_ERROR_NOT_FOUND;
}
g_convertProgress = 1.0f;
return SAVE_DATA_OK;
}
int APS5_VABI sceSaveDataConvert(const SaveDataConvert* param) {
const int rc = convert(param);
SAVEDATA_TRACE("convert user=%d dir=%s -> 0x%08x", param != nullptr ? param->user_id : -1, param != nullptr && param->dir_name != nullptr ? param->dir_name->data : "(null)", static_cast<unsigned>(rc));
return rc;
}
static int getConvertProgress(float* progress) {
if (progress == nullptr) {
return SAVE_DATA_ERROR_PARAMETER;
}
if (g_initializations == 0) {
return SAVE_DATA_ERROR_NOT_INITIALIZED;
}
*progress = g_convertProgress.load();
return SAVE_DATA_OK;
}
int APS5_VABI sceSaveDataGetConvertProgress(float* progress) {
const int rc = getConvertProgress(progress);
SAVEDATA_TRACE("getConvertProgress -> 0x%08x progress=%.2f", static_cast<unsigned>(rc), progress != nullptr && rc == SAVE_DATA_OK ? *progress : 0.0f);
return rc;
}
int APS5_VABI sceSaveDataCancel() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceSaveDataDirNameSearch2(const SaveDataDirNameSearchCond* cond, SaveDataDirNameSearchResult* result) {
const int rc = dirNameSearch(cond, result);
SAVEDATA_TRACE("dirNameSearch2 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<unsigned>(rc));
return rc;
}
APS5_EXPORT("RvID8BVeg+g", sceSaveDataUnknown_RvID8BVeg);
int APS5_VABI sceSaveDataUnknown_RvID8BVeg() {
NotImplemented_nid_no_patch("RvID8BVeg+g");
return 0;
}
}