diff --git a/CMakeLists.txt b/CMakeLists.txt index 3b56b982d..b4f6c71ef 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -232,6 +232,8 @@ set(SYSTEM_LIBS src/core/libraries/system/commondialog.cpp src/core/libraries/system/msgdialog_ui.cpp src/core/libraries/system/posix.cpp src/core/libraries/system/posix.h + src/core/libraries/save_data/save_backup.cpp + src/core/libraries/save_data/save_backup.h src/core/libraries/save_data/save_instance.cpp src/core/libraries/save_data/save_instance.h src/core/libraries/save_data/savedata.cpp diff --git a/src/core/file_format/psf.cpp b/src/core/file_format/psf.cpp index f37461ae0..48802ca8c 100644 --- a/src/core/file_format/psf.cpp +++ b/src/core/file_format/psf.cpp @@ -51,6 +51,8 @@ PSF& PSF::operator=(PSF&& other) noexcept { } bool PSF::Open(const std::filesystem::path& filepath) { + last_write = std::filesystem::last_write_time(filepath); + Common::FS::IOFile file(filepath, Common::FS::FileAccessMode::Read); if (!file.IsOpen()) { return false; @@ -125,6 +127,8 @@ bool PSF::Encode(const std::filesystem::path& filepath) const { return false; } + last_write = std::filesystem::file_time_type::clock::now(); + const auto psf_buffer = Encode(); return file.Write(psf_buffer) == psf_buffer.size(); } diff --git a/src/core/file_format/psf.h b/src/core/file_format/psf.h index 017fe87da..373b66c7e 100644 --- a/src/core/file_format/psf.h +++ b/src/core/file_format/psf.h @@ -70,11 +70,17 @@ public: void AddString(std::string key, std::string value, bool update = false); void AddInteger(std::string key, s32 value, bool update = false); + [[nodiscard]] std::filesystem::file_time_type GetLastWrite() const { + return last_write; + } + [[nodiscard]] const std::vector& GetEntries() const { return entry_list; } private: + mutable std::filesystem::file_time_type last_write; + std::vector entry_list; std::unordered_map> map_binaries; diff --git a/src/core/libraries/save_data/save_backup.cpp b/src/core/libraries/save_data/save_backup.cpp new file mode 100644 index 000000000..60e4858ea --- /dev/null +++ b/src/core/libraries/save_data/save_backup.cpp @@ -0,0 +1,199 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include + +#include + +#include "save_backup.h" +#include "save_instance.h" + +#include "common/logging/log.h" +#include "common/logging/log_entry.h" +#include "common/polyfill_thread.h" +#include "common/thread.h" + +constexpr std::string_view sce_sys = "sce_sys"; // system folder inside save +constexpr std::string_view backup_dir = "sce_backup"; // backup folder +constexpr std::string_view backup_dir_tmp = "sce_backup_tmp"; // in-progress backup folder + +namespace fs = std::filesystem; + +namespace Libraries::SaveData::Backup { + +static std::jthread g_backup_thread; +static std::counting_semaphore g_backup_thread_semaphore{0}; + +static std::mutex g_backup_queue_mutex; +static std::deque g_backup_queue; +static std::deque g_result_queue; + +static std::atomic_int g_backup_progress = 0; +static std::atomic g_backup_status = WorkerStatus::NotStarted; + +static void backup(const std::filesystem::path& dir_name) { + if (!fs::exists(dir_name)) { + return; + } + std::vector backup_files; + for (const auto& entry : fs::directory_iterator(dir_name)) { + const auto filename = entry.path().filename(); + if (filename != backup_dir && filename != backup_dir_tmp) { + backup_files.push_back(entry.path()); + } + } + const auto backup_dir = dir_name / ::backup_dir; + const auto backup_dir_tmp = dir_name / ::backup_dir_tmp; + + g_backup_progress = 0; + + int total_count = static_cast(backup_files.size()); + int current_count = 0; + + fs::remove_all(backup_dir_tmp); + fs::create_directory(backup_dir_tmp); + for (const auto& file : backup_files) { + fs::copy(file, backup_dir_tmp / file.filename(), fs::copy_options::recursive); + current_count++; + g_backup_progress = current_count * 100 / total_count; + } + bool has_existing = fs::exists(backup_dir); + if (has_existing) { + fs::rename(backup_dir, dir_name / "sce_backup_old"); + } + fs::rename(backup_dir_tmp, backup_dir); + if (has_existing) { + fs::remove_all(dir_name / "sce_backup_old"); + } +} + +static void BackupThreadBody() { + Common::SetCurrentThreadName("SaveData_BackupThread"); + while (true) { + g_backup_status = WorkerStatus::Waiting; + g_backup_thread_semaphore.acquire(); + BackupRequest req; + { + std::scoped_lock lk{g_backup_queue_mutex}; + req = g_backup_queue.front(); + } + if (req.save_path.empty()) { + break; + } + g_backup_status = WorkerStatus::Running; + LOG_INFO(Lib_SaveData, "Backing up the following directory: {}", req.save_path.string()); + backup(req.save_path); + LOG_DEBUG(Lib_SaveData, "Backing up the following directory: {} finished", + req.save_path.string()); + { + std::scoped_lock lk{g_backup_queue_mutex}; + g_backup_queue.pop_front(); + g_result_queue.push_back(std::move(req)); + if (g_result_queue.size() > 20) { + g_result_queue.pop_front(); + } + } + } + g_backup_status = WorkerStatus::NotStarted; +} + +void StartThread() { + if (g_backup_status != WorkerStatus::NotStarted) { + return; + } + LOG_DEBUG(Lib_SaveData, "Starting backup thread"); + g_backup_thread = std::jthread{BackupThreadBody}; + g_backup_status = WorkerStatus::Waiting; +} + +void StopThread() { + if (g_backup_status == WorkerStatus::NotStarted || g_backup_status == WorkerStatus::Stopping) { + return; + } + LOG_DEBUG(Lib_SaveData, "Stopping backup thread"); + { + std::scoped_lock lk{g_backup_queue_mutex}; + g_backup_queue.emplace_back(BackupRequest{}); + } + g_backup_thread_semaphore.release(); + g_backup_status = WorkerStatus::Stopping; +} + +bool NewRequest(OrbisUserServiceUserId user_id, std::string_view title_id, + std::string_view dir_name, OrbisSaveDataEventType origin) { + auto save_path = SaveInstance::MakeDirSavePath(user_id, title_id, dir_name); + + if (g_backup_status != WorkerStatus::Waiting && g_backup_status != WorkerStatus::Running) { + LOG_ERROR(Lib_SaveData, "Called backup while status is {}. Backup request to {} ignored", + magic_enum::enum_name(g_backup_status.load()), save_path.string()); + return false; + } + { + std::scoped_lock lk{g_backup_queue_mutex}; + g_backup_queue.push_back(BackupRequest{ + .user_id = user_id, + .title_id = std::string{title_id}, + .dir_name = std::string{dir_name}, + .origin = origin, + .save_path = std::move(save_path), + }); + } + g_backup_thread_semaphore.release(); + return true; +} + +bool Restore(const std::filesystem::path& save_path) { + LOG_INFO(Lib_SaveData, "Restoring backup for {}", save_path.string()); + if (!fs::exists(save_path) || !fs::exists(save_path / backup_dir)) { + return false; + } + for (const auto& entry : fs::directory_iterator(save_path)) { + const auto filename = entry.path().filename(); + if (filename != backup_dir) { + fs::remove_all(entry.path()); + } + } + + for (const auto& entry : fs::directory_iterator(save_path / backup_dir)) { + const auto filename = entry.path().filename(); + fs::copy(entry.path(), save_path / filename, fs::copy_options::recursive); + } + + return true; +} + +WorkerStatus GetWorkerStatus() { + return g_backup_status; +} + +bool IsBackupExecutingFor(const std::filesystem::path& save_path) { + std::scoped_lock lk{g_backup_queue_mutex}; + return std::ranges::find(g_backup_queue, save_path, + [](const auto& v) { return v.save_path; }) != g_backup_queue.end(); +} + +std::filesystem::path MakeBackupPath(const std::filesystem::path& save_path) { + return save_path / backup_dir; +} + +std::optional PopLastEvent() { + std::scoped_lock lk{g_backup_queue_mutex}; + if (g_result_queue.empty()) { + return std::nullopt; + } + auto req = std::move(g_result_queue.front()); + g_result_queue.pop_front(); + return req; +} + +float GetProgress() { + return static_cast(g_backup_progress) / 100.0f; +} + +void ClearProgress() { + g_backup_progress = 0; +} + +} // namespace Libraries::SaveData::Backup \ No newline at end of file diff --git a/src/core/libraries/save_data/save_backup.h b/src/core/libraries/save_data/save_backup.h new file mode 100644 index 000000000..bcf9f5ab1 --- /dev/null +++ b/src/core/libraries/save_data/save_backup.h @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "common/types.h" + +namespace Libraries::SaveData { + +using OrbisUserServiceUserId = s32; + +namespace Backup { + +enum class WorkerStatus { + NotStarted, + Waiting, + Running, + Stopping, +}; + +enum class OrbisSaveDataEventType : u32 { + UMOUNT_BACKUP = 1, + BACKUP = 2, + SAVE_DATA_MEMORY_SYNC = 3, +}; + +struct BackupRequest { + OrbisUserServiceUserId user_id{}; + std::string title_id{}; + std::string dir_name{}; + OrbisSaveDataEventType origin{}; + + std::filesystem::path save_path{}; +}; + +// No problem calling this function if the backup thread is already running +void StartThread(); + +void StopThread(); + +bool NewRequest(OrbisUserServiceUserId user_id, std::string_view title_id, + std::string_view dir_name, OrbisSaveDataEventType origin); + +bool Restore(const std::filesystem::path& save_path); + +WorkerStatus GetWorkerStatus(); + +bool IsBackupExecutingFor(const std::filesystem::path& save_path); + +std::filesystem::path MakeBackupPath(const std::filesystem::path& save_path); + +std::optional PopLastEvent(); + +float GetProgress(); + +void ClearProgress(); + +} // namespace Backup + +} // namespace Libraries::SaveData diff --git a/src/core/libraries/save_data/save_instance.cpp b/src/core/libraries/save_data/save_instance.cpp index 29a13cc1c..18ba2d771 100644 --- a/src/core/libraries/save_data/save_instance.cpp +++ b/src/core/libraries/save_data/save_instance.cpp @@ -113,7 +113,6 @@ SaveInstance& SaveInstance::operator=(SaveInstance&& other) noexcept { param_sfo_path = std::move(other.param_sfo_path); corrupt_file_path = std::move(other.corrupt_file_path); corrupt_file = std::move(other.corrupt_file); - last_write = other.last_write; param_sfo = std::move(other.param_sfo); mount_point = std::move(other.mount_point); max_blocks = other.max_blocks; @@ -140,14 +139,12 @@ void SaveInstance::SetupAndMount(bool read_only, bool copy_icon, bool ignore_cor } } exists = true; - last_write = std::filesystem::file_time_type::clock::now(); } else { if (!ignore_corrupt && fs::exists(corrupt_file_path)) { throw std::filesystem::filesystem_error( "Corrupted save data", corrupt_file_path, std::make_error_code(std::errc::illegal_byte_sequence)); } - last_write = fs::last_write_time(param_sfo_path); if (!param_sfo.Open(param_sfo_path)) { throw std::filesystem::filesystem_error( "Failed to read param.sfo", param_sfo_path, @@ -155,7 +152,7 @@ void SaveInstance::SetupAndMount(bool read_only, bool copy_icon, bool ignore_cor } } - if (!ignore_corrupt) { + if (!ignore_corrupt && !read_only) { int err = corrupt_file.Open(corrupt_file_path, Common::FS::FileAccessMode::Write); if (err != 0) { throw std::filesystem::filesystem_error( @@ -183,7 +180,6 @@ void SaveInstance::Umount() { throw std::filesystem::filesystem_error("Failed to write param.sfo", param_sfo_path, std::make_error_code(std::errc::permission_denied)); } - last_write = std::filesystem::file_time_type::clock::now(); param_sfo = PSF(); corrupt_file.Close(); diff --git a/src/core/libraries/save_data/save_instance.h b/src/core/libraries/save_data/save_instance.h index 83d22abf3..fc6edb59e 100644 --- a/src/core/libraries/save_data/save_instance.h +++ b/src/core/libraries/save_data/save_instance.h @@ -5,6 +5,7 @@ #include +#include "common/io_file.h" #include "core/file_format/psf.h" namespace Core::FileSys { @@ -43,7 +44,6 @@ class SaveInstance { Common::FS::IOFile corrupt_file; - std::filesystem::file_time_type last_write; PSF param_sfo; std::string mount_point; @@ -90,6 +90,14 @@ public: return exists; } + [[nodiscard]] OrbisUserServiceUserId GetUserId() const noexcept { + return user_id; + } + + [[nodiscard]] std::string_view GetTitleId() const noexcept { + return game_serial; + } + [[nodiscard]] const std::string& GetDirName() const noexcept { return dir_name; } @@ -98,10 +106,6 @@ public: return save_path; } - [[nodiscard]] const std::filesystem::file_time_type& GetLastWrite() const noexcept { - return last_write; - } - [[nodiscard]] const PSF& GetParamSFO() const noexcept { return param_sfo; } diff --git a/src/core/libraries/save_data/savedata.cpp b/src/core/libraries/save_data/savedata.cpp index 74023f000..7b639ff40 100644 --- a/src/core/libraries/save_data/savedata.cpp +++ b/src/core/libraries/save_data/savedata.cpp @@ -18,6 +18,7 @@ #include "core/libraries/error_codes.h" #include "core/libraries/libs.h" #include "core/libraries/save_data/savedata.h" +#include "save_backup.h" #include "save_instance.h" namespace fs = std::filesystem; @@ -56,12 +57,12 @@ constexpr u32 OrbisSaveDataBlocksMax = 32768; // 1 GiB constexpr size_t OrbisSaveDataMountPointDataMaxsize = 16; // Maximum size for a title ID (4 uppercase letters + 5 digits) constexpr int OrbisSaveDataTitleIdDataSize = 10; -// Maximum size for a save data directory name -constexpr int OrbisSaveDataDirnameDataMaxsize = 32; -constexpr size_t OrbisSaveDataTitleMaxsize = 128; -constexpr size_t OrbisSaveDataSubtitleMaxsize = 128; -constexpr size_t OrbisSaveDataDetailMaxsize = 1024; +constexpr int OrbisSaveDataDirnameDataMaxsize = 32; // Maximum save directory name size +constexpr size_t OrbisSaveDataTitleMaxsize = 128; // Maximum title name size +constexpr size_t OrbisSaveDataSubtitleMaxsize = 128; // Maximum subtitle name size +constexpr size_t OrbisSaveDataDetailMaxsize = 1024; // Maximum detail name size +constexpr size_t OrbisSaveDataFingerprintDataSize = 65; // Maximum fingerprint size constexpr std::string_view OrbisSaveDataDirnameSaveDataMemory = "sce_sdmemory"; constexpr std::string_view OrbisSaveDataFilenameSaveDataMemory = "memory.dat"; @@ -120,6 +121,40 @@ struct OrbisSaveDataParam { int : 32; time_t mtime; std::array _reserved; + + void FromSFO(const PSF& sfo) { + memset(this, 0, sizeof(OrbisSaveDataParam)); + title.FromString(*sfo.GetString(SaveParams::MAINTITLE)); + subTitle.FromString(*sfo.GetString(SaveParams::SUBTITLE)); + detail.FromString(*sfo.GetString(SaveParams::DETAIL)); + userParam = sfo.GetInteger(SaveParams::SAVEDATA_LIST_PARAM).value_or(0); + const auto time_since_epoch = sfo.GetLastWrite().time_since_epoch(); + mtime = chrono::duration_cast(time_since_epoch).count(); + } +}; + +struct OrbisSaveDataFingerprint { + CString data; + std::array _pad; +}; + +struct OrbisSaveDataBackup { + OrbisUserServiceUserId userId; + s32 : 32; + const OrbisSaveDataTitleId* titleId; + const OrbisSaveDataDirName* dirName; + const OrbisSaveDataFingerprint* param; + std::array _reserved; +}; + +struct OrbisSaveDataCheckBackupData { + OrbisUserServiceUserId userId; + s32 : 32; + const OrbisSaveDataTitleId* titleId; + const OrbisSaveDataDirName* dirName; + OrbisSaveDataParam* param; + OrbisSaveDataIcon* icon; + std::array _reserved; }; struct OrbisSaveDataDelete { @@ -137,6 +172,19 @@ struct OrbisSaveDataIcon { size_t bufSize; size_t dataSize; std::array _reserved; + + Error LoadIcon(const std::filesystem::path& icon_path) { + try { + const Common::FS::IOFile file(icon_path, Common::FS::FileAccessMode::Read); + dataSize = file.GetSize(); + file.Seek(0); + file.ReadRaw(buf, std::min(bufSize, dataSize)); + } catch (const fs::filesystem_error& e) { + LOG_ERROR(Lib_SaveData, "Failed to load icon: {}", e.what()); + return Error::INTERNAL; + } + return Error::OK; + } }; struct OrbisSaveDataMount2 { @@ -179,6 +227,17 @@ struct OrbisSaveDataMountResult { s32 : 32; }; +struct OrbisSaveDataRestoreBackupData { + OrbisUserServiceUserId userId; + s32 : 32; + const OrbisSaveDataTitleId* titleId; + const OrbisSaveDataDirName* dirName; + const OrbisSaveDataFingerprint* fingerprint; + u32 _unused; + std::array _reserved; + s32 : 32; +}; + struct OrbisSaveDataDirNameSearchCond { OrbisUserServiceUserId userId; int : 32; @@ -207,6 +266,22 @@ struct OrbisSaveDataDirNameSearchResult { int : 32; }; +struct OrbisSaveDataEventParam { // dummy structure + OrbisSaveDataEventParam() = delete; +}; + +using OrbisSaveDataEventType = Backup::OrbisSaveDataEventType; + +struct OrbisSaveDataEvent { + OrbisSaveDataEventType type; + s32 errorCode; + OrbisUserServiceUserId userId; + std::array _pad; + OrbisSaveDataTitleId titleId; + OrbisSaveDataDirName dirName; + std::array _reserved; +}; + bool is_rw_mode = false; static bool g_initialized = false; @@ -252,16 +327,23 @@ static Error saveDataMount(const OrbisSaveDataMount2* mount_info, return Error::INVALID_LOGIN_USER; } if (mount_info->blocks < OrbisSaveDataBlocksMin2 || - mount_info->blocks > OrbisSaveDataBlocksMax) { + mount_info->blocks > OrbisSaveDataBlocksMax || mount_info->dirName == nullptr) { LOG_INFO(Lib_SaveData, "called with invalid parameter"); return Error::PARAMETER; } + // check backup status + { + const auto save_path = SaveInstance::MakeDirSavePath(mount_info->userId, g_game_serial, + mount_info->dirName->data); + if (Backup::IsBackupExecutingFor(save_path)) { + return Error::BACKUP_BUSY; + } + } + auto mount_mode = mount_info->mountMode; const bool is_ro = True(mount_mode & OrbisSaveDataMountMode::RDONLY); - if (!is_ro) { - ASSERT(True(mount_mode & OrbisSaveDataMountMode::RDWR)); - } + const bool create = True(mount_mode & OrbisSaveDataMountMode::CREATE); const bool create_if_not_exist = True(mount_mode & OrbisSaveDataMountMode::CREATE2); ASSERT(!create || !create_if_not_exist); // Can't have both @@ -335,14 +417,69 @@ static Error saveDataMount(const OrbisSaveDataMount2* mount_info, return Error::OK; } +static Error Umount(const OrbisSaveDataMountPoint* mountPoint, bool call_backup = false) { + if (!g_initialized) { + LOG_INFO(Lib_SaveData, "called without initialize"); + return Error::NOT_INITIALIZED; + } + if (mountPoint == nullptr) { + LOG_INFO(Lib_SaveData, "called with invalid parameter"); + return Error::PARAMETER; + } + LOG_DEBUG(Lib_SaveData, "Umount mountPoint:{}", mountPoint->data.to_view()); + const std::string_view mount_point_str{mountPoint->data}; + for (auto& instance : g_mount_slots) { + if (instance.has_value()) { + const auto& slot_name = instance->GetMountPoint(); + if (slot_name == mount_point_str) { + if (call_backup) { + Backup::StartThread(); + Backup::NewRequest(instance->GetUserId(), instance->GetTitleId(), + instance->GetDirName(), + OrbisSaveDataEventType::UMOUNT_BACKUP); + } + // TODO: check if is busy + instance->Umount(); + instance.reset(); + return Error::OK; + } + } + } + return Error::NOT_FOUND; +} + int PS4_SYSV_ABI sceSaveDataAbort() { LOG_ERROR(Lib_SaveData, "(STUBBED) called"); return ORBIS_OK; } -int PS4_SYSV_ABI sceSaveDataBackup() { - LOG_ERROR(Lib_SaveData, "(STUBBED) called"); - return ORBIS_OK; +Error PS4_SYSV_ABI sceSaveDataBackup(const OrbisSaveDataBackup* backup) { + if (!g_initialized) { + LOG_INFO(Lib_SaveData, "called without initialize"); + return Error::NOT_INITIALIZED; + } + if (backup == nullptr || backup->dirName == nullptr) { + LOG_INFO(Lib_SaveData, "called with invalid parameter"); + return Error::PARAMETER; + } + + const std::string_view dir_name{backup->dirName->data}; + LOG_DEBUG(Lib_SaveData, "called dirName: {}", dir_name); + + std::string_view title{backup->titleId != nullptr ? std::string_view{backup->titleId->data} + : std::string_view{g_game_serial}}; + + for (const auto& instance : g_mount_slots) { + if (instance.has_value() && instance->GetUserId() == backup->userId && + instance->GetTitleId() == title && instance->GetDirName() == dir_name) { + return Error::BUSY; + } + } + + Backup::StartThread(); + Backup::NewRequest(backup->userId, title, dir_name, OrbisSaveDataEventType::BACKUP); + + return Error::OK; } int PS4_SYSV_ABI sceSaveDataBindPsnAccount() { @@ -365,9 +502,54 @@ int PS4_SYSV_ABI sceSaveDataChangeInternal() { return ORBIS_OK; } -int PS4_SYSV_ABI sceSaveDataCheckBackupData(/*const OrbisSaveDataCheckBackupData* check*/) { - LOG_ERROR(Lib_SaveData, "(STUBBED) called"); - return ORBIS_OK; +Error PS4_SYSV_ABI sceSaveDataCheckBackupData(const OrbisSaveDataCheckBackupData* check) { + if (!g_initialized) { + LOG_INFO(Lib_SaveData, "called without initialize"); + return Error::NOT_INITIALIZED; + } + if (check == nullptr || check->dirName == nullptr) { + LOG_INFO(Lib_SaveData, "called with invalid parameter"); + return Error::PARAMETER; + } + + const std::string_view title{check->titleId != nullptr ? std::string_view{check->titleId->data} + : std::string_view{g_game_serial}}; + + const auto save_path = + SaveInstance::MakeDirSavePath(check->userId, title, check->dirName->data); + + for (const auto& instance : g_mount_slots) { + if (instance.has_value() && instance->GetSavePath() == save_path) { + return Error::BUSY; + } + } + + if (Backup::IsBackupExecutingFor(save_path)) { + return Error::BACKUP_BUSY; + } + + const auto backup_path = Backup::MakeBackupPath(save_path); + if (!fs::exists(backup_path)) { + return Error::NOT_FOUND; + } + + if (check->param != nullptr) { + PSF sfo; + if (!sfo.Open(backup_path / "sce_sys" / "param.sfo")) { + LOG_ERROR(Lib_SaveData, "Failed to read SFO at {}", backup_path.string()); + return Error::INTERNAL; + } + check->param->FromSFO(sfo); + } + + if (check->icon != nullptr) { + const auto icon_path = backup_path / "sce_sys" / "icon0.png"; + if (fs::exists(icon_path) && check->icon->LoadIcon(icon_path) != Error::OK) { + return Error::INTERNAL; + } + } + + return Error::OK; } int PS4_SYSV_ABI sceSaveDataCheckBackupDataForCdlg() { @@ -405,9 +587,14 @@ int PS4_SYSV_ABI sceSaveDataCheckSaveDataVersionLatest() { return ORBIS_OK; } -int PS4_SYSV_ABI sceSaveDataClearProgress() { - LOG_ERROR(Lib_SaveData, "(STUBBED) called"); - return ORBIS_OK; +Error PS4_SYSV_ABI sceSaveDataClearProgress() { + if (!g_initialized) { + LOG_INFO(Lib_SaveData, "called without initialize"); + return Error::NOT_INITIALIZED; + } + LOG_DEBUG(Lib_SaveData, "called"); + Backup::ClearProgress(); + return Error::OK; } int PS4_SYSV_ABI sceSaveDataCopy5() { @@ -546,7 +733,6 @@ Error PS4_SYSV_ABI sceSaveDataDirNameSearch(const OrbisSaveDataDirNameSearchCond std::unordered_map map_dir_sfo; std::unordered_map map_max_blocks; - std::unordered_map map_mtime; std::unordered_map map_free_size; for (const auto& dir_name : dir_list) { @@ -563,10 +749,6 @@ Error PS4_SYSV_ABI sceSaveDataDirNameSearch(const OrbisSaveDataDirNameSearchCond size_t total = SaveInstance::GetMaxBlocks(dir_path); map_free_size.emplace(dir_name, total - size / OrbisSaveDataBlockSize); map_max_blocks.emplace(dir_name, total); - - const auto last_write = fs::last_write_time(sfo_path).time_since_epoch(); - size_t mtime = chrono::duration_cast(last_write).count(); - map_mtime.emplace(dir_name, mtime); } #define PROJ(x) [&](const auto& v) { return x; } @@ -586,7 +768,7 @@ Error PS4_SYSV_ABI sceSaveDataDirNameSearch(const OrbisSaveDataDirNameSearchCond std::ranges::stable_sort(dir_list, {}, PROJ(map_free_size.at(v))); break; case OrbisSaveDataSortKey::MTIME: - std::ranges::stable_sort(dir_list, {}, PROJ(map_mtime.at(v))); + std::ranges::stable_sort(dir_list, {}, PROJ(map_dir_sfo.at(v).GetLastWrite())); break; } #undef PROJ @@ -605,14 +787,8 @@ Error PS4_SYSV_ABI sceSaveDataDirNameSearch(const OrbisSaveDataDirNameSearchCond if (result->params != nullptr) { auto& sfo = map_dir_sfo.at(dir_list[i]); - auto& last_write = map_mtime.at(dir_list[i]); auto& param_data = result->params[i]; - memset(¶m_data, 0, sizeof(OrbisSaveDataParam)); - param_data.title.FromString(*sfo.GetString(SaveParams::MAINTITLE)); - param_data.subTitle.FromString(*sfo.GetString(SaveParams::SUBTITLE)); - param_data.detail.FromString(*sfo.GetString(SaveParams::DETAIL)); - param_data.userParam = sfo.GetInteger(SaveParams::SAVEDATA_LIST_PARAM).value_or(0); - param_data.mtime = last_write; + param_data.FromSFO(sfo); } if (result->infos != nullptr) { @@ -685,10 +861,30 @@ int PS4_SYSV_ABI sceSaveDataGetEventInfo() { return ORBIS_OK; } -int PS4_SYSV_ABI sceSaveDataGetEventResult(/*const OrbisSaveDataEventParam* eventParam, - OrbisSaveDataEvent* event*/) { - LOG_ERROR(Lib_SaveData, "(STUBBED) called"); - return ORBIS_OK; +Error PS4_SYSV_ABI sceSaveDataGetEventResult(const OrbisSaveDataEventParam*, + OrbisSaveDataEvent* event) { + if (!g_initialized) { + LOG_INFO(Lib_SaveData, "called without initialize"); + return Error::NOT_INITIALIZED; + } + if (event == nullptr) { + LOG_INFO(Lib_SaveData, "called with invalid parameter"); + return Error::PARAMETER; + } + LOG_DEBUG(Lib_SaveData, "called"); + + auto last_event = Backup::PopLastEvent(); + if (!last_event.has_value()) { + return Error::NOT_FOUND; + } + + event->type = last_event->origin; + event->errorCode = 0; + event->userId = last_event->user_id; + event->titleId.data.FromString(last_event->title_id); + event->dirName.data.FromString(last_event->dir_name); + + return Error::OK; } int PS4_SYSV_ABI sceSaveDataGetFormat() { @@ -738,13 +934,11 @@ Error PS4_SYSV_ABI sceSaveDataGetParam(const OrbisSaveDataMountPoint* mountPoint } LOG_DEBUG(Lib_SaveData, "called: paramType = {}", magic_enum::enum_name(paramType)); const PSF* param_sfo = nullptr; - std::filesystem::file_time_type last_write{}; const std::string_view mount_point_str{mountPoint->data}; for (const auto& instance : g_mount_slots) { if (instance.has_value() && instance->GetMountPoint() == mount_point_str) { param_sfo = &instance->GetParamSFO(); - last_write = instance->GetLastWrite(); break; } } @@ -756,13 +950,7 @@ Error PS4_SYSV_ABI sceSaveDataGetParam(const OrbisSaveDataMountPoint* mountPoint case OrbisSaveDataParamType::ALL: { const auto param = static_cast(paramBuf); ASSERT(paramBufSize == sizeof(OrbisSaveDataParam)); - memset(param, 0, sizeof(OrbisSaveDataParam)); - param->title.FromString(*param_sfo->GetString(SaveParams::MAINTITLE)); - param->subTitle.FromString(*param_sfo->GetString(SaveParams::SUBTITLE)); - param->detail.FromString(*param_sfo->GetString(SaveParams::DETAIL)); - param->userParam = param_sfo->GetInteger(SaveParams::SAVEDATA_LIST_PARAM).value_or(0); - param->mtime = - chrono::duration_cast(last_write.time_since_epoch()).count(); + param->FromSFO(*param_sfo); if (gotSize != nullptr) { *gotSize = sizeof(OrbisSaveDataParam); } @@ -797,7 +985,8 @@ Error PS4_SYSV_ABI sceSaveDataGetParam(const OrbisSaveDataMountPoint* mountPoint } break; case OrbisSaveDataParamType::MTIME: { const auto param = static_cast(paramBuf); - *param = chrono::duration_cast(last_write.time_since_epoch()).count(); + const auto last_write = param_sfo->GetLastWrite().time_since_epoch(); + *param = chrono::duration_cast(last_write).count(); if (gotSize != nullptr) { *gotSize = sizeof(time_t); } @@ -809,9 +998,18 @@ Error PS4_SYSV_ABI sceSaveDataGetParam(const OrbisSaveDataMountPoint* mountPoint return Error::OK; } -int PS4_SYSV_ABI sceSaveDataGetProgress() { - LOG_ERROR(Lib_SaveData, "(STUBBED) called"); - return ORBIS_OK; +Error PS4_SYSV_ABI sceSaveDataGetProgress(float* progress) { + if (!g_initialized) { + LOG_INFO(Lib_SaveData, "called without initialize"); + return Error::NOT_INITIALIZED; + } + if (progress == nullptr) { + LOG_INFO(Lib_SaveData, "called with invalid parameter"); + return Error::PARAMETER; + } + LOG_DEBUG(Lib_SaveData, "called"); + *progress = Backup::GetProgress(); + return Error::OK; } int PS4_SYSV_ABI sceSaveDataGetSaveDataCount() { @@ -924,17 +1122,7 @@ Error PS4_SYSV_ABI sceSaveDataLoadIcon(const OrbisSaveDataMountPoint* mountPoint return Error::NOT_FOUND; } - try { - const Common::FS::IOFile file(path, Common::FS::FileAccessMode::Read); - icon->dataSize = file.GetSize(); - file.Seek(0); - file.ReadRaw(icon->buf, std::min(icon->bufSize, icon->dataSize)); - } catch (const fs::filesystem_error& e) { - LOG_ERROR(Lib_SaveData, "Failed to load icon: {}", e.what()); - return Error::INTERNAL; - } - - return Error::OK; + return icon->LoadIcon(path); } Error PS4_SYSV_ABI sceSaveDataMount(const OrbisSaveDataMount* mount, @@ -1003,9 +1191,44 @@ int PS4_SYSV_ABI sceSaveDataRegisterEventCallback() { return ORBIS_OK; } -int PS4_SYSV_ABI sceSaveDataRestoreBackupData() { - LOG_ERROR(Lib_SaveData, "(STUBBED) called"); - return ORBIS_OK; +Error PS4_SYSV_ABI sceSaveDataRestoreBackupData(const OrbisSaveDataRestoreBackupData* restore) { + if (!g_initialized) { + LOG_INFO(Lib_SaveData, "called without initialize"); + return Error::NOT_INITIALIZED; + } + if (restore == nullptr || restore->dirName == nullptr) { + LOG_INFO(Lib_SaveData, "called with invalid parameter"); + return Error::PARAMETER; + } + + const std::string_view dir_name{restore->dirName->data}; + LOG_DEBUG(Lib_SaveData, "called dirName: {}", dir_name); + + std::string_view title{restore->titleId != nullptr ? std::string_view{restore->titleId->data} + : std::string_view{g_game_serial}}; + + const auto save_path = SaveInstance::MakeDirSavePath(restore->userId, title, dir_name); + + for (const auto& instance : g_mount_slots) { + if (instance.has_value() && instance->GetSavePath() == save_path) { + return Error::BUSY; + } + } + if (Backup::IsBackupExecutingFor(save_path)) { + return Error::BACKUP_BUSY; + } + + const auto backup_path = Backup::MakeBackupPath(save_path); + if (!fs::exists(backup_path)) { + return Error::NOT_FOUND; + } + + const bool ok = Backup::Restore(save_path); + if (!ok) { + return Error::INTERNAL; + } + + return Error::OK; } int PS4_SYSV_ABI sceSaveDataRestoreBackupDataForCdlg() { @@ -1185,6 +1408,7 @@ Error PS4_SYSV_ABI sceSaveDataTerminate() { } } g_initialized = false; + Backup::StopThread(); return Error::OK; } @@ -1194,28 +1418,8 @@ int PS4_SYSV_ABI sceSaveDataTransferringMount() { } Error PS4_SYSV_ABI sceSaveDataUmount(const OrbisSaveDataMountPoint* mountPoint) { - if (!g_initialized) { - LOG_INFO(Lib_SaveData, "called without initialize"); - return Error::NOT_INITIALIZED; - } - if (mountPoint == nullptr) { - LOG_INFO(Lib_SaveData, "called with invalid parameter"); - return Error::PARAMETER; - } - LOG_DEBUG(Lib_SaveData, "called mountPoint:{}", mountPoint->data.to_view()); - const std::string_view mount_point_str{mountPoint->data}; - for (auto& instance : g_mount_slots) { - if (instance.has_value()) { - const auto& slot_name = instance->GetMountPoint(); - if (slot_name == mount_point_str) { - // TODO: check if is busy - instance->Umount(); - instance.reset(); - return Error::OK; - } - } - } - return Error::NOT_FOUND; + LOG_DEBUG(Lib_SaveData, "called"); + return Umount(mountPoint); } int PS4_SYSV_ABI sceSaveDataUmountSys() { @@ -1223,9 +1427,9 @@ int PS4_SYSV_ABI sceSaveDataUmountSys() { return ORBIS_OK; } -int PS4_SYSV_ABI sceSaveDataUmountWithBackup(const OrbisSaveDataMountPoint* mountPoint) { - LOG_ERROR(Lib_SaveData, "(STUBBED) called"); - return ORBIS_OK; +Error PS4_SYSV_ABI sceSaveDataUmountWithBackup(const OrbisSaveDataMountPoint* mountPoint) { + LOG_DEBUG(Lib_SaveData, "called"); + return Umount(mountPoint, true); } int PS4_SYSV_ABI sceSaveDataUnregisterEventCallback() { diff --git a/src/core/libraries/save_data/savedata.h b/src/core/libraries/save_data/savedata.h index a21b9692b..83fc4ac2d 100644 --- a/src/core/libraries/save_data/savedata.h +++ b/src/core/libraries/save_data/savedata.h @@ -10,25 +10,23 @@ class SymbolsResolver; } namespace Libraries::SaveData { - -constexpr int ORBIS_SAVE_DATA_FINGERPRINT_DATA_SIZE = 65; -struct OrbisSaveDataFingerprint { - char data[ORBIS_SAVE_DATA_FINGERPRINT_DATA_SIZE]; - char padding[15]; -}; - enum class Error : u32; enum class OrbisSaveDataParamType : u32; +struct OrbisSaveDataBackup; +struct OrbisSaveDataCheckBackupData; struct OrbisSaveDataDelete; +struct OrbisSaveDataDirNameSearchCond; +struct OrbisSaveDataDirNameSearchResult; +struct OrbisSaveDataEvent; +struct OrbisSaveDataEventParam; struct OrbisSaveDataIcon; struct OrbisSaveDataMount2; struct OrbisSaveDataMount; struct OrbisSaveDataMountInfo; struct OrbisSaveDataMountPoint; struct OrbisSaveDataMountResult; -struct OrbisSaveDataDirNameSearchCond; -struct OrbisSaveDataDirNameSearchResult; +struct OrbisSaveDataRestoreBackupData; /*typedef u32 OrbisSaveDataSaveDataMemoryOption; #define ORBIS_SAVE_DATA_MEMORY_OPTION_NONE (0x00000000) @@ -51,22 +49,6 @@ struct OrbisSaveDataMemorySetupResult { u8 reserved[16]; }; -typedef u32 OrbisSaveDataEventType; -#define SCE_SAVE_DATA_EVENT_TYPE_INVALID (0) -#define SCE_SAVE_DATA_EVENT_TYPE_UMOUNT_BACKUP_END (1) -#define SCE_SAVE_DATA_EVENT_TYPE_BACKUP_END (2) -#define SCE_SAVE_DATA_EVENT_TYPE_SAVE_DATA_MEMORY_SYNC_END (3) - -struct OrbisSaveDataEvent { - OrbisSaveDataEventType type; - s32 errorCode; - s32 userId; - u8 padding[4]; - OrbisSaveDataTitleId titleId; - OrbisSaveDataDirName dirName; - u8 reserved[40]; -}; - struct OrbisSaveDataMemoryData { void* buf; size_t bufSize; @@ -95,18 +77,6 @@ struct OrbisSaveDataMemorySet2 { u8 reserved[24]; }; -struct OrbisSaveDataCheckBackupData { - s32 userId; - int : 32; - const OrbisSaveDataTitleId* titleId; - const OrbisSaveDataDirName* dirName; - OrbisSaveDataParam* param; - OrbisSaveDataIcon* icon; - u8 reserved[32]; -}; - -typedef struct _OrbisSaveDataEventParam OrbisSaveDataEventParam; - typedef u32 OrbisSaveDataMemorySyncOption; #define SCE_SAVE_DATA_MEMORY_SYNC_OPTION_NONE (0x00000000) @@ -127,12 +97,12 @@ constexpr int ORBIS_SAVE_DATA_PARAM_TYPE_USER_PARAM = 4; constexpr int ORBIS_SAVE_DATA_PARAM_TYPE_MTIME = 5;*/ int PS4_SYSV_ABI sceSaveDataAbort(); -int PS4_SYSV_ABI sceSaveDataBackup(); +Error PS4_SYSV_ABI sceSaveDataBackup(const OrbisSaveDataBackup* backup); int PS4_SYSV_ABI sceSaveDataBindPsnAccount(); int PS4_SYSV_ABI sceSaveDataBindPsnAccountForSystemBackup(); int PS4_SYSV_ABI sceSaveDataChangeDatabase(); int PS4_SYSV_ABI sceSaveDataChangeInternal(); -int PS4_SYSV_ABI sceSaveDataCheckBackupData(/*const OrbisSaveDataCheckBackupData* check*/); +Error PS4_SYSV_ABI sceSaveDataCheckBackupData(const OrbisSaveDataCheckBackupData* check); int PS4_SYSV_ABI sceSaveDataCheckBackupDataForCdlg(); int PS4_SYSV_ABI sceSaveDataCheckBackupDataInternal(); int PS4_SYSV_ABI sceSaveDataCheckCloudData(); @@ -140,7 +110,7 @@ int PS4_SYSV_ABI sceSaveDataCheckIpmiIfSize(); int PS4_SYSV_ABI sceSaveDataCheckSaveDataBroken(); int PS4_SYSV_ABI sceSaveDataCheckSaveDataVersion(); int PS4_SYSV_ABI sceSaveDataCheckSaveDataVersionLatest(); -int PS4_SYSV_ABI sceSaveDataClearProgress(); +Error PS4_SYSV_ABI sceSaveDataClearProgress(); int PS4_SYSV_ABI sceSaveDataCopy5(); int PS4_SYSV_ABI sceSaveDataCreateUploadData(); int PS4_SYSV_ABI sceSaveDataDebug(); @@ -169,8 +139,8 @@ int PS4_SYSV_ABI sceSaveDataGetClientThreadPriority(); int PS4_SYSV_ABI sceSaveDataGetCloudQuotaInfo(); int PS4_SYSV_ABI sceSaveDataGetDataBaseFilePath(); int PS4_SYSV_ABI sceSaveDataGetEventInfo(); -int PS4_SYSV_ABI sceSaveDataGetEventResult(/*const OrbisSaveDataEventParam* eventParam, - OrbisSaveDataEvent* event*/); +Error PS4_SYSV_ABI sceSaveDataGetEventResult(const OrbisSaveDataEventParam* eventParam, + OrbisSaveDataEvent* event); int PS4_SYSV_ABI sceSaveDataGetFormat(); int PS4_SYSV_ABI sceSaveDataGetMountedSaveDataCount(); Error PS4_SYSV_ABI sceSaveDataGetMountInfo(const OrbisSaveDataMountPoint* mountPoint, @@ -178,7 +148,7 @@ Error PS4_SYSV_ABI sceSaveDataGetMountInfo(const OrbisSaveDataMountPoint* mountP Error PS4_SYSV_ABI sceSaveDataGetParam(const OrbisSaveDataMountPoint* mountPoint, OrbisSaveDataParamType paramType, void* paramBuf, size_t paramBufSize, size_t* gotSize); -int PS4_SYSV_ABI sceSaveDataGetProgress(); +Error PS4_SYSV_ABI sceSaveDataGetProgress(float* progress); int PS4_SYSV_ABI sceSaveDataGetSaveDataCount(); int PS4_SYSV_ABI sceSaveDataGetSaveDataMemory(const u32 userId, void* buf, const size_t bufSize, const int64_t offset); @@ -206,7 +176,7 @@ int PS4_SYSV_ABI sceSaveDataMountSys(); int PS4_SYSV_ABI sceSaveDataPromote5(); int PS4_SYSV_ABI sceSaveDataRebuildDatabase(); int PS4_SYSV_ABI sceSaveDataRegisterEventCallback(); -int PS4_SYSV_ABI sceSaveDataRestoreBackupData(); +Error PS4_SYSV_ABI sceSaveDataRestoreBackupData(const OrbisSaveDataRestoreBackupData* restore); int PS4_SYSV_ABI sceSaveDataRestoreBackupDataForCdlg(); int PS4_SYSV_ABI sceSaveDataRestoreLoadSaveDataMemory(); Error PS4_SYSV_ABI sceSaveDataSaveIcon(const OrbisSaveDataMountPoint* mountPoint, @@ -232,7 +202,7 @@ Error PS4_SYSV_ABI sceSaveDataTerminate(); int PS4_SYSV_ABI sceSaveDataTransferringMount(); Error PS4_SYSV_ABI sceSaveDataUmount(const OrbisSaveDataMountPoint* mountPoint); int PS4_SYSV_ABI sceSaveDataUmountSys(); -int PS4_SYSV_ABI sceSaveDataUmountWithBackup(const OrbisSaveDataMountPoint* mountPoint); +Error PS4_SYSV_ABI sceSaveDataUmountWithBackup(const OrbisSaveDataMountPoint* mountPoint); int PS4_SYSV_ABI sceSaveDataUnregisterEventCallback(); int PS4_SYSV_ABI sceSaveDataUpload(); int PS4_SYSV_ABI Func_02E4C4D201716422();