#include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include #include #include #include #include #include #include extern "C" { int APS5_VABI sceSaveDataDirNameSearch(const SaveDataDirNameSearchCond*, SaveDataDirNameSearchResult*); int APS5_VABI sceSaveDataDirNameSearch2(const SaveDataDirNameSearchCond*, SaveDataDirNameSearchResult*); 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); } static constexpr int SaveDataErrorParameter = -2137063424; static void Require(bool value, const char* message) { if (!value) throw std::runtime_error(message); } static std::vector Search(std::uint32_t key, std::uint32_t order, std::uint32_t capacity, std::uint32_t* hits) { SceSaveDataDirName pattern{}; std::snprintf(pattern.data, sizeof(pattern.data), "SORT_%%"); SaveDataDirNameSearchCond cond{}; cond.user_id = 1; cond.dir_name = &pattern; cond.key = key; cond.order = order; std::array names{}; SaveDataDirNameSearchResult result{}; result.dir_names = names.data(); result.dir_names_num = capacity; Require(sceSaveDataDirNameSearch(&cond, &result) == 0, "sorted search failed"); *hits = result.hit_num; std::vector found; for (std::uint32_t i = 0; i < result.set_num; ++i) found.emplace_back(names[i].data); return found; } static void SortByKey() { struct Save { const char* name; std::uint32_t userParam; std::int64_t mtime; }; const std::array saves{{{"SORT_B", 1, 1000}, {"SORT_A", 3, 2000}, {"SORT_C", 2, 3000}}}; for (const auto& save : saves) { std::filesystem::create_directories(std::filesystem::path("_sd") / save.name); SceSaveDataDirName dirName{}; std::snprintf(dirName.data, sizeof(dirName.data), "%s", save.name); SaveDataMount3 mount{}; mount.user_id = 1; mount.dir_name = &dirName; mount.mount_mode = 2; SaveDataMountResult mounted{}; Require(sceSaveDataMount3(&mount, &mounted) == 0, "mount for parameters failed"); Require(sceSaveDataSetParam(&mounted.mount_point, 4, &save.userParam, sizeof(save.userParam)) == 0, "user parameter was not set"); Require(sceSaveDataSetParam(&mounted.mount_point, 5, &save.mtime, sizeof(save.mtime)) == 0, "mtime was not set"); Require(sceSaveDataUmount2(0, &mounted.mount_point) == 0, "unmount failed"); } using Names = std::vector; std::uint32_t hits = 0; Require(Search(0, 0, 3, &hits) == Names{"SORT_A", "SORT_B", "SORT_C"} && hits == 3, "directory names must sort ascending"); Require(Search(0, 1, 3, &hits) == Names{"SORT_C", "SORT_B", "SORT_A"}, "directory names must sort descending"); Require(Search(1, 0, 3, &hits) == Names{"SORT_B", "SORT_C", "SORT_A"}, "user parameters must sort ascending"); Require(Search(1, 1, 3, &hits) == Names{"SORT_A", "SORT_C", "SORT_B"}, "user parameters must sort descending"); Require(Search(3, 0, 3, &hits) == Names{"SORT_B", "SORT_A", "SORT_C"}, "modification times must sort ascending"); Require(Search(3, 1, 1, &hits) == Names{"SORT_C"} && hits == 3, "the newest save must come first in descending modification time"); SceSaveDataDirName pattern{}; std::snprintf(pattern.data, sizeof(pattern.data), "SORT_%%"); SaveDataDirNameSearchCond second{}; second.user_id = 1; second.dir_name = &pattern; second.key = 1; second.order = 1; std::array secondNames{}; SaveDataDirNameSearchResult secondResult{}; secondResult.dir_names = secondNames.data(); secondResult.dir_names_num = 3; Require(sceSaveDataDirNameSearch2(&second, &secondResult) == 0, "the second search failed"); Require(secondResult.hit_num == 3 && secondResult.set_num == 3, "the second search must return all three saves"); Require(std::string(secondNames[0].data) == "SORT_A" && std::string(secondNames[1].data) == "SORT_C" && std::string(secondNames[2].data) == "SORT_B", "the second search must sort like the first"); second.key = 6; Require(sceSaveDataDirNameSearch2(&second, &secondResult) == SaveDataErrorParameter, "the second search must reject an unknown sort key"); SaveDataDirNameSearchCond cond{}; cond.user_id = 1; cond.key = 6; SaveDataDirNameSearchResult result{}; Require(sceSaveDataDirNameSearch(&cond, &result) == SaveDataErrorParameter, "an unknown sort key must be rejected"); cond.key = 0; cond.order = 2; Require(sceSaveDataDirNameSearch(&cond, &result) == SaveDataErrorParameter, "an unknown sort order must be rejected"); } static std::vector SearchPattern(const char* text) { SceSaveDataDirName pattern{}; std::snprintf(pattern.data, sizeof(pattern.data), "%s", text); SaveDataDirNameSearchCond cond{}; cond.user_id = 1; cond.dir_name = &pattern; std::array names{}; SaveDataDirNameSearchResult result{}; result.dir_names = names.data(); result.dir_names_num = static_cast(names.size()); Require(sceSaveDataDirNameSearch(&cond, &result) == 0, "pattern search failed"); std::vector found; for (std::uint32_t i = 0; i < result.set_num; ++i) found.emplace_back(names[i].data); return found; } static void TrailingWildcards() { for (const char* name : std::array{"TRAIL", "TRAIL1"}) std::filesystem::create_directories(std::filesystem::path("_sd") / name); using Names = std::vector; Require(SearchPattern("TRAIL%") == Names{"TRAIL", "TRAIL1"}, "a trailing % must also match no characters"); Require(SearchPattern("TRAIL%%") == Names{"TRAIL", "TRAIL1"}, "trailing %% must also match no characters"); Require(SearchPattern("TRAI_%") == Names{"TRAIL", "TRAIL1"}, "_ then % must match one character and then none"); Require(SearchPattern("TRAIL_%") == Names{"TRAIL1"}, "_ must still match exactly one character"); } static void SearchAndMount() { const std::array names{std::string(31, 'A'), std::string(31, 'B')}; for (const auto& name : names) std::filesystem::create_directories(std::filesystem::path("_sd") / name); alignas(SceSaveDataDirName) std::array buffer; buffer.fill('!'); SaveDataDirNameSearchCond cond{}; cond.user_id = 1; SaveDataDirNameSearchResult result{}; result.dir_names = reinterpret_cast(buffer.data()); result.dir_names_num = 2; alignas(SaveDataSearchInfo) std::array infos; infos.fill(0x21); result.infos = reinterpret_cast(infos.data()); Require(sceSaveDataDirNameSearch(&cond, &result) == 0, "search failed"); Require(result.hit_num == 2 && result.set_num == 2, "search must return both saves"); for (std::size_t i = 64; i < buffer.size(); ++i) { Require(buffer[i] == '!', "directory search overran two 32-byte guest records"); } for (std::size_t i = 96; i < infos.size(); ++i) { Require(infos[i] == 0x21, "directory search overran two 48-byte search infos"); } for (std::size_t i = 0; i < 2; ++i) { SaveDataSearchInfo info{}; std::memcpy(&info, infos.data() + i * 48, sizeof(info)); Require(info.blocks == 32768 && info.free_blocks == 32768, "search info must report the save's blocks and free blocks"); for (const auto byte : info.reserved) Require(byte == 0, "search info reserved bytes must be zero"); } std::array found{}; for (int i = 0; i < 2; ++i) { const char* record = buffer.data() + i * 32; Require(record[31] == '\0', "directory record must terminate within 32 bytes"); const bool first = std::strcmp(record, names[0].c_str()) == 0; Require(first || std::strcmp(record, names[1].c_str()) == 0, "returned directory name is corrupted"); const int index = first ? 0 : 1; Require(!found[index], "search returned a duplicate directory"); found[index] = true; SaveDataMount3 mount{}; mount.user_id = cond.user_id; mount.dir_name = reinterpret_cast(record); mount.mount_mode = 1; SaveDataMountResult mounted{}; Require(sceSaveDataMount3(&mount, &mounted) == 0, "returned directory must be mountable"); 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() / ("anyps5-savedata-search-" + std::to_string(std::random_device{}())); std::filesystem::create_directories(root); std::filesystem::current_path(root); int status = 0; try { SearchAndMount(); SortByKey(); TrailingWildcards(); } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); status = 1; } std::filesystem::current_path(previous); std::filesystem::remove_all(root); return status; }