diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 08cc546d3..269106de8 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -219,6 +219,10 @@ int APS5_VABI sceSaveDataCreateTransactionResource(uint32_t size) { return g_transaction_counter.fetch_add(1); } +static std::string blocks_path(const std::string& real_path) { + return real_path + ".blocks"; +} + static int deleteSave(const SaveDataDelete* del) { if (del == nullptr || del->dir_name == nullptr) { throw std::runtime_error("sceSaveDataDelete: null argument"); @@ -254,6 +258,7 @@ static int deleteSave(const SaveDataDelete* del) { } std::error_code ec; std::filesystem::remove(param_path(path), ec); + std::filesystem::remove(blocks_path(path), ec); return SAVE_DATA_OK; } @@ -268,6 +273,45 @@ int APS5_VABI sceSaveDataDeleteTransactionResource(int32_t resource) { return SAVE_DATA_OK; } +static constexpr std::uint64_t SAVE_DATA_BLOCK_BYTES = 32768; + +static std::uint64_t save_blocks(const std::string& real_path) { + std::ifstream file(blocks_path(real_path)); + std::uint64_t blocks = 0; + if (!(file >> blocks) || blocks == 0) { + return SAVE_DATA_BLOCKS_MAX; + } + 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 std::uint64_t used_blocks(const std::string& real_path) { + std::uint64_t blocks = 0; + std::error_code error; + for (auto it = std::filesystem::recursive_directory_iterator(real_path, error); !error && it != std::filesystem::recursive_directory_iterator(); it.increment(error)) { + if (!it->is_regular_file(error)) { + continue; + } + const auto bytes = it->file_size(error); + if (!error) { + blocks += (bytes + SAVE_DATA_BLOCK_BYTES - 1) / SAVE_DATA_BLOCK_BYTES; + } + } + return blocks; +} + +static SaveDataSearchInfo search_info(const std::string& real_path) { + const auto blocks = save_blocks(real_path); + return SaveDataSearchInfo{blocks, blocks - std::min(blocks, used_blocks(real_path)), {}}; +} + static int dirNameSearch(const SaveDataDirNameSearchCond* cond, SaveDataDirNameSearchResult* result) { if (cond == nullptr || result == nullptr) { throw std::runtime_error("sceSaveDataDirNameSearch: null argument"); @@ -287,6 +331,7 @@ static int dirNameSearch(const SaveDataDirNameSearchCond* cond, SaveDataDirNameS std::string name; std::string path; SaveDataParam param; + SaveDataSearchInfo info; }; const bool sortByParam = key == SAVE_DATA_SORT_KEY_USER_PARAM || key == SAVE_DATA_SORT_KEY_MTIME; std::vector matches; @@ -298,7 +343,7 @@ static int dirNameSearch(const SaveDataDirNameSearchCond* cond, SaveDataDirNameS if (!dir_name_match(name.c_str(), pattern)) { continue; } - Match match{name, entry.path().string(), {}}; + Match match{name, entry.path().string(), {}, search_info(entry.path().string())}; if (sortByParam) { match.param = load_param(match.path); } @@ -309,6 +354,10 @@ static int dirNameSearch(const SaveDataDirNameSearchCond* cond, SaveDataDirNameS 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; }); + } else if (key == SAVE_DATA_SORT_KEY_BLOCKS) { + std::stable_sort(matches.begin(), matches.end(), [](const Match& a, const Match& b) { return a.info.blocks < b.info.blocks; }); + } else if (key == SAVE_DATA_SORT_KEY_FREE_BLOCKS) { + std::stable_sort(matches.begin(), matches.end(), [](const Match& a, const Match& b) { return a.info.free_blocks < b.info.free_blocks; }); } if (cond->order == SAVE_DATA_SORT_ORDER_DESCENT) { std::reverse(matches.begin(), matches.end()); @@ -320,7 +369,7 @@ static int dirNameSearch(const SaveDataDirNameSearchCond* cond, SaveDataDirNameS 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->infos[i] = matches[i].info; } } result->hit_num = static_cast(matches.size()); @@ -353,12 +402,14 @@ static int getMountInfo(const SaveDataMountPoint* mount_point, SaveDataMountInfo throw std::runtime_error("sceSaveDataGetMountInfo: null argument"); } std::lock_guard lock(g_slots_mutex); - if (find_slot_by_mount_point(mount_point->data) == -1) { + const int slot = find_slot_by_mount_point(mount_point->data); + if (slot == -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; + const auto search = search_info(g_slots[slot].real_path); + info->blocks = search.blocks; + info->free_blocks = search.free_blocks; return SAVE_DATA_OK; } @@ -512,6 +563,10 @@ 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; } diff --git a/core/libs/tests/GuestSaveDataSearch.cpp b/core/libs/tests/GuestSaveDataSearch.cpp index 53d0cc506..066dbd7df 100644 --- a/core/libs/tests/GuestSaveDataSearch.cpp +++ b/core/libs/tests/GuestSaveDataSearch.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -16,6 +17,7 @@ int APS5_VABI sceSaveDataDirNameSearch2(const SaveDataDirNameSearchCond*, SaveDa int APS5_VABI sceSaveDataMount3(const SaveDataMount3*, SaveDataMountResult*); int APS5_VABI sceSaveDataUmount2(std::uint32_t, const SaveDataMountPoint*); int APS5_VABI sceSaveDataSetParam(const SaveDataMountPoint*, std::uint32_t, const void*, std::size_t); +int APS5_VABI sceSaveDataGetMountInfo(const SaveDataMountPoint*, SaveDataMountInfo*); } static constexpr int SaveDataErrorParameter = -2137063424; @@ -171,6 +173,68 @@ static void SearchAndMount() { } } +static void CreateSized(const char* name, std::uint64_t blocks, std::size_t bytes) { + SceSaveDataDirName dirName{}; + std::snprintf(dirName.data, sizeof(dirName.data), "%s", name); + SaveDataMount3 mount{}; + mount.user_id = 1; + mount.dir_name = &dirName; + mount.blocks = blocks; + mount.mount_mode = 2 | 4; + SaveDataMountResult mounted{}; + Require(sceSaveDataMount3(&mount, &mounted) == 0, "creating a sized save failed"); + if (bytes != 0) { + std::ofstream file(std::filesystem::path("_sd") / name / "data", std::ios::binary); + file << std::string(bytes, 'x'); + } + Require(sceSaveDataUmount2(0, &mounted.mount_point) == 0, "unmount failed"); +} + +static std::vector SizeSearch(std::uint32_t key, std::vector& found) { + SceSaveDataDirName pattern{}; + std::snprintf(pattern.data, sizeof(pattern.data), "SIZE_%%"); + SaveDataDirNameSearchCond cond{}; + cond.user_id = 1; + cond.dir_name = &pattern; + cond.key = key; + std::array names{}; + std::array infos{}; + SaveDataDirNameSearchResult result{}; + result.dir_names = names.data(); + result.dir_names_num = 2; + result.infos = infos.data(); + Require(sceSaveDataDirNameSearch(&cond, &result) == 0 && result.set_num == 2, "the sized search must return both saves"); + found.clear(); + for (const auto& name : names) found.emplace_back(name.data); + return {infos.begin(), infos.end()}; +} + +static void SaveSize() { + CreateSized("SIZE_A", 200, 0); + CreateSized("SIZE_B", 96, 40000); + std::vector found; + auto infos = SizeSearch(0, found); + Require(found == std::vector{"SIZE_A", "SIZE_B"}, "the sized saves must sort by name"); + Require(infos[0].blocks == 200 && infos[0].free_blocks == 200, "an empty save must report all its blocks free"); + Require(infos[1].blocks == 96 && infos[1].free_blocks == 94, "a save must report the blocks it was created with, less those its files take"); + SizeSearch(2, found); + Require(found == std::vector{"SIZE_B", "SIZE_A"}, "saves must sort by blocks"); + infos = SizeSearch(5, found); + Require(found == std::vector{"SIZE_B", "SIZE_A"} && infos[0].free_blocks < infos[1].free_blocks, "saves must sort by free blocks"); + SceSaveDataDirName dirName{}; + std::snprintf(dirName.data, sizeof(dirName.data), "SIZE_B"); + SaveDataMount3 mount{}; + mount.user_id = 1; + mount.dir_name = &dirName; + mount.mount_mode = 1; + SaveDataMountResult mounted{}; + Require(sceSaveDataMount3(&mount, &mounted) == 0, "mounting the sized save failed"); + SaveDataMountInfo info{}; + Require(sceSaveDataGetMountInfo(&mounted.mount_point, &info) == 0, "mount info failed"); + Require(info.blocks == 96 && info.free_blocks == 94, "mount info must report the save's blocks and free blocks"); + Require(sceSaveDataUmount2(0, &mounted.mount_point) == 0, "unmount failed"); +} + int main() { const auto previous = std::filesystem::current_path(); const auto root = std::filesystem::temp_directory_path() / @@ -182,6 +246,7 @@ int main() { SearchAndMount(); SortByKey(); TrailingWildcards(); + SaveSize(); } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); status = 1; diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index 45da9c14c..c8a63945c 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -7,7 +7,7 @@ - [sceSaveDataConvert](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the parameter block (user id at `+0x00`, `SceSaveDataDirName` pointer at `+0x08`, 48 opaque bytes after it) is read from PPSA28416's single call, not confirmed by KytyPS5, shadPS4, fpPS4 or SharpProspero, which have no body for it. Host saves have no size limit, so an existing save returns `SCE_OK` without touching its files and no event is queued; what the console returns for a save that is mounted, already converted or needs no conversion is unknown. The directory name is validated as `sceSaveDataDelete` does - [sceSaveDataGetConvertProgress](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - imported by PPSA16064; signature and error codes follow the [SDK 12.000 reference](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-get-convert-progress.md) (`float* progress`). Save data conversion on host filesystems completes synchronously, so the progress query returns `SAVE_DATA_OK` with the last conversion's progress (`0.0f` before any conversion or when conversion fails, `1.0f` after successful conversion), `SAVE_DATA_ERROR_PARAMETER` if `progress` is null, or `SAVE_DATA_ERROR_NOT_INITIALIZED` when the library is uninitialized; intermediate asynchronous conversion progress on console is unmeasured - [sceSaveDataCancel](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - imported by PPSA23126, which fails to load without it; added without an implementation because its signature is unknown. Every save data call here finishes before it returns, so nothing is ever in flight to cancel, but what the console returns then is unknown -- [sceSaveDataDirNameSearch](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the sort keys (0 directory name, 1 user parameter, 2 blocks, 3 modification time, 5 free blocks) and orders (0 ascending, 1 descending) follow shadPS4, not a hardware run; saves with equal keys keep directory-name order before a descending reverse. Every native save reports `SAVE_DATA_BLOCKS_MAX` blocks, as `sceSaveDataGetMountInfo` does, so the two block keys keep directory-name order. The search infos (blocks, free blocks and 32 reserved bytes per returned name, following shadPS4's `OrbisSaveDataSearchInfo`) report `SAVE_DATA_BLOCKS_MAX` for both. Keys other than 0, 1, 2, 3 and 5 (including 4) and orders above 1 return `SCE_SAVE_DATA_ERROR_PARAMETER`; shadPS4 and KytyPS5 only reject keys above 5, so key 4 is rejected here because no source names it. In the directory name pattern `%` matches any run of characters, none included, and `_` exactly one, as the comments of shadPS4's matcher describe; also not confirmed on hardware +- [sceSaveDataDirNameSearch](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the sort keys (0 directory name, 1 user parameter, 2 blocks, 3 modification time, 5 free blocks) and orders (0 ascending, 1 descending) follow shadPS4, not a hardware run; saves with equal keys keep directory-name order before a descending reverse. A save's size is the `blocks` of the `sceSaveDataMount3` call that created it, kept beside the save in `.blocks`; a save without a kept size takes that of its next mount that names one, and otherwise reports `SAVE_DATA_BLOCKS_MAX`. The search infos (blocks, free blocks and 32 reserved bytes per returned name, following shadPS4's `OrbisSaveDataSearchInfo`), `sceSaveDataGetMountInfo` and the two block keys use that size and, as free, what the save's files leave of it, each file rounded up to 32 KiB blocks; the console's file system overhead is not known and not counted. Keys other than 0, 1, 2, 3 and 5 (including 4) and orders above 1 return `SCE_SAVE_DATA_ERROR_PARAMETER`; shadPS4 and KytyPS5 only reject keys above 5, so key 4 is rejected here because no source names it. In the directory name pattern `%` matches any run of characters, none included, and `_` exactly one, as the comments of shadPS4's matcher describe; also not confirmed on hardware - [sceSaveDataInitialize3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a repeated initialize succeeds: PPSA12544's executable and its Unity SaveData plugin both initialize, and the plugin fails on any error. Initializations are counted and `sceSaveDataTerminate` ends the session at the last one; how the console pairs them is unknown ### Functional