diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 21b323c9e11d..5c8091e70db3 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -332,16 +332,19 @@ static std::string blocks_path(const std::string& real_path) { return real_path + ".blocks"; } -static bool isMountedDirectory(const std::string& path) { +static bool isMountedDirectory(const std::string& path, std::error_code& error) { for (const auto& slot : g_slots) { if (!slot.used) continue; if (slot.real_path == path) return true; - if (!std::filesystem::exists(path) || !std::filesystem::exists(slot.real_path)) continue; - std::error_code error; + const bool exists = std::filesystem::exists(path, error); + if (error) return false; + if (!exists) continue; + const bool mountedExists = std::filesystem::exists(slot.real_path, error); + if (error) return false; + if (!mountedExists) continue; const bool sameDirectory = std::filesystem::equivalent(slot.real_path, path, error); - if (error && error != std::errc::no_such_file_or_directory) { - throw std::filesystem::filesystem_error("SaveData: failed to compare save paths", slot.real_path, path, error); - } + if (error == std::errc::no_such_file_or_directory) error.clear(); + if (error) return false; if (sameDirectory) return true; } return false; @@ -361,7 +364,9 @@ static int deleteSave(const SaveDataDelete* del) { } const std::string path = save_root() + "/" + dirName; std::lock_guard lock(g_slots_mutex); - if (isMountedDirectory(path)) return SAVE_DATA_ERROR_BUSY; + std::error_code error; + if (isMountedDirectory(path, error)) return SAVE_DATA_ERROR_BUSY; + if (error) return SAVE_DATA_ERROR_INTERNAL; if (std::filesystem::is_directory(path)) { RemoveTree(path); } @@ -394,12 +399,9 @@ static std::uint64_t save_blocks(const std::string& real_path) { return std::min(blocks, SAVE_DATA_BLOCKS_MAX); } -static void keep_save_blocks(const std::string& real_path, std::uint64_t blocks) { - std::ofstream file(blocks_path(real_path), std::ios::trunc); - file << std::min(blocks, SAVE_DATA_BLOCKS_MAX) << '\n'; - if (!file) { - throw std::runtime_error("sceSaveDataMount3: the save's size could not be kept"); - } +static bool keep_save_blocks(const std::string& real_path, std::uint64_t blocks) { + const auto content = std::to_string(std::min(blocks, SAVE_DATA_BLOCKS_MAX)) + '\n'; + return savedata::replace_file(blocks_path(real_path), content.data(), content.size()); } static std::uint64_t used_blocks(const std::string& real_path) { @@ -651,8 +653,12 @@ static int mount3(const SaveDataMount3* mount, SaveDataMountResult* mount_result } const std::string real_path = save_root() + "/" + dirName; std::lock_guard lock(g_slots_mutex); - if (isMountedDirectory(real_path)) return SAVE_DATA_ERROR_BUSY; - const bool exists = std::filesystem::is_directory(real_path); + std::error_code error; + if (isMountedDirectory(real_path, error)) return SAVE_DATA_ERROR_BUSY; + if (error) return SAVE_DATA_ERROR_INTERNAL; + const bool exists = std::filesystem::is_directory(real_path, error); + if (error == std::errc::no_such_file_or_directory) error.clear(); + if (error) return SAVE_DATA_ERROR_INTERNAL; if (create && exists) { return SAVE_DATA_ERROR_EXISTS; } @@ -663,13 +669,26 @@ static int mount3(const SaveDataMount3* mount, SaveDataMountResult* mount_result if (slot == -1) { return SAVE_DATA_ERROR_MOUNT_FULL; } - if (create || create2) { - std::filesystem::create_directories(real_path); + bool keepSize = mount->blocks != 0 && (create || (create2 && !exists)); + if (mount->blocks != 0 && !keepSize) { + keepSize = !std::filesystem::exists(blocks_path(real_path), error); + if (error) return SAVE_DATA_ERROR_INTERNAL; + } + const auto absolutePath = std::filesystem::absolute(real_path, error).string(); + if (error) return SAVE_DATA_ERROR_INTERNAL; + const bool created = (create || create2) && std::filesystem::create_directories(real_path, error); + if (error) return SAVE_DATA_ERROR_INTERNAL; + if (keepSize && !keep_save_blocks(real_path, mount->blocks)) { + if (created) { + std::error_code ignored; + std::filesystem::remove(real_path, ignored); + } + return SAVE_DATA_ERROR_INTERNAL; } // 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()); + AddPathAlias_nid_no_patch(mountPoint.c_str(), absolutePath.c_str()); std::memset(mount_result, 0, sizeof(*mount_result)); g_slots[slot].used = true; g_slots[slot].mount_point = mountPoint; @@ -677,16 +696,12 @@ static int mount3(const SaveDataMount3* mount, SaveDataMountResult* mount_result 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; - std::error_code sized; - if (mount->blocks != 0 && (create || (create2 && !exists) || !std::filesystem::exists(blocks_path(real_path), sized))) { - keep_save_blocks(real_path, mount->blocks); - } 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, static_cast(sizeof(SceSaveDataDirName::data)), 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), static_cast(sizeof(SaveDataMountPoint::data)), mount_result != nullptr ? mount_result->mount_point.data : "", mount_result != nullptr ? mount_result->mount_status : 0u); + SAVEDATA_TRACE("mount3 user=%d dir=%.*s mode=0x%x blocks=%llu -> 0x%08x point=%.*s status=%u", mount != nullptr ? mount->user_id : -1, static_cast(sizeof(SceSaveDataDirName::data)), 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), static_cast(sizeof(SaveDataMountPoint::data)), rc == SAVE_DATA_OK && mount_result != nullptr ? mount_result->mount_point.data : "", rc == SAVE_DATA_OK && mount_result != nullptr ? mount_result->mount_status : 0u); return rc; } diff --git a/core/libs/tests/GuestSaveDataMountStatus.cpp b/core/libs/tests/GuestSaveDataMountStatus.cpp index 8370e3a5aec3..a4b1e81ae66f 100644 --- a/core/libs/tests/GuestSaveDataMountStatus.cpp +++ b/core/libs/tests/GuestSaveDataMountStatus.cpp @@ -1,6 +1,7 @@ #include "prx/libSceSaveData/include/Api.hpp" #include "prx/libSceSaveData.native/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/General.hpp" #include "SceTypes.hpp" #include #include @@ -316,6 +317,64 @@ void CheckMountCapacityAndLifecycle() { for (const auto& mount : mounted) Check(sceSaveDataUmount2(0, &mount.mount_point) == 0, "capacity unmount succeeds"); } +void CheckHostMountFailures() { + const auto previous = std::filesystem::current_path(); + const auto root = previous / "mount-host-failures"; + std::filesystem::create_directories(root / "previous-alias"); + std::filesystem::current_path(root); + const auto writeFixture = [](const char* path, const char* text) { + auto* file = std::fopen(path, "wb"); + Check(file != nullptr, std::string("open fixture ") + path); + if (file == nullptr) return; + const bool written = std::fputs(text, file) >= 0; + const bool closed = std::fclose(file) == 0; + Check(written && closed, std::string("write fixture ") + path); + }; + const auto checkFailure = [&](const char* name, std::uint32_t mode, std::uint64_t blocks, const std::string& label) { + SceSaveDataDirName directory{}; + std::snprintf(directory.data, sizeof(directory.data), "%s", name); + SaveDataMount3 request{}; + request.dir_name = &directory; + request.mount_mode = mode; + request.blocks = blocks; + SaveDataMountResult result; + std::memset(&result, 0xCD, sizeof(result)); + const auto before = result; + const auto alias = std::filesystem::absolute("previous-alias").string(); + AddPathAlias_nid_no_patch("/savedata0", alias.c_str()); + const auto resolvedBefore = ResolvePath_nid_no_patch("/savedata0/probe"); + try { + Check(sceSaveDataMount3(&request, &result) == static_cast(0x809F000B), label + " returns INTERNAL"); + } catch (const std::exception& error) { + Check(false, label + " throws: " + error.what()); + } + Check(std::memcmp(&result, &before, sizeof(result)) == 0, label + " preserves the mount result"); + Check(ResolvePath_nid_no_patch("/savedata0/probe") == resolvedBefore, label + " preserves the path alias"); + SaveDataMountPoint point{}; + std::memcpy(point.data, "/savedata0", sizeof("/savedata0")); + SaveDataMountInfo info{}; + const auto mounted = sceSaveDataGetMountInfo(&point, &info); + Check(mounted == SaveDataErrorNotMounted, label + " consumes no mount slot"); + if (mounted == 0) sceSaveDataUmount2(0, &point); + RemovePathAlias_nid_no_patch("/savedata0"); + }; + writeFixture("_sd", "not a directory"); + for (const auto mode : {MountReadWrite | MountCreate, MountReadWrite | MountCreate2}) { + checkFailure("blocked", mode, 96, "save-root file with mode " + std::to_string(mode)); + Check(std::filesystem::is_regular_file("_sd"), "a failed mount preserves the blocking file"); + } + Check(std::filesystem::remove("_sd"), "remove the blocked save-root fixture"); + std::filesystem::create_directories("_sd/metadata.blocks"); + writeFixture("_sd/metadata.blocks/keep", "preserve this directory"); + checkFailure("metadata", MountReadWrite | MountCreate2, 96, "size metadata path is a directory"); + Check(!std::filesystem::exists("_sd/metadata"), "failed size persistence removes the newly created empty save"); + Check(std::filesystem::is_regular_file("_sd/metadata.blocks/keep"), "failed size persistence preserves the blocking directory"); + std::filesystem::remove_all("_sd/metadata.blocks"); + MountAndCheck("metadata", MountReadWrite | MountCreate2, 1u, "mount after repairing host storage"); + std::filesystem::current_path(previous); + std::filesystem::remove_all(root); +} + } int main() { @@ -328,6 +387,7 @@ int main() { CheckUnmountedCalls(); Check(sceSaveDataSyncSaveDataMemory(nullptr) == static_cast(0x809F0000), "null param rejected in memory sync"); Check(sceSaveDataInitialize3(nullptr) == 0, "SaveData initializes"); + CheckHostMountFailures(); CheckMountCapacityAndLifecycle(); MountAndCheck("fresh", MountReadWrite | MountCreate2, 1u, "CREATE2 of a missing save"); MountAndCheck("fresh", MountReadWrite | MountCreate2, 0u, "CREATE2 of an existing save"); diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index f5d1375b9c64..457623ab7951 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -2,6 +2,8 @@ ### Silent stubs +- [sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp): host filesystem failures and a failed block-count sidecar replacement return the existing INTERNAL error (0x809F000B), following the host-I/O policy used by other save operations. The corresponding PS5 error mapping for each host failure is not confirmed; directory creation and sidecar replacement must finish before exposing a mount. + - [sceSaveDataAbort, sceSaveDataClearProgress](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) return `SAVE_DATA_OK` after initialization without cancelling operations or clearing any progress. The current host operations run synchronously and no backup progress is tracked; `sceSaveDataClearProgress` does not reset `sceSaveDataGetConvertProgress`. Their console signatures, errors and behavior are unverified. - [sceSaveDataPrepare, sceSaveDataCommit](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) return success without looking at their arguments; what preparing and committing a mounted save do on a console is unknown. The same six titles call both; this counts tests of the result in the three instructions after each call. `sceSaveDataPrepare` has 11 call sites: PPSA01494, PPSA30839 and PPSA31162 test none of them there, PPSA03984 takes an error path when it is negative, PPSA34502 checks it at one of two sites and PPSA36591 at two of four. `sceSaveDataCommit` has 14: PPSA01494 and PPSA30839 test none, PPSA03984 and PPSA31162 check its sign, PPSA34502 checks it at all five sites and PPSA36591 at two of four. A failure code from either would change the save flow of the titles that check it, while the stubs keep all six on the success path.