Merge pull request #3771 from yunusshaikh7/fix/savedata-dir-name-search-sort

fix(savedata): sort sceSaveDataDirNameSearch results by the requested key and order
This commit is contained in:
Adrià 2026-10-12 00:30:16 +02:00 committed by GitHub
commit dd1cf5cb9d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 104 additions and 10 deletions

View File

@ -270,13 +270,22 @@ static int dirNameSearch(const SaveDataDirNameSearchCond* cond, SaveDataDirNameS
}
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;
}
std::uint32_t hit = 0;
std::uint32_t set = 0;
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;
@ -285,16 +294,29 @@ static int dirNameSearch(const SaveDataDirNameSearchCond* cond, SaveDataDirNameS
if (!dir_name_match(name.c_str(), pattern)) {
continue;
}
hit++;
if (result->dir_names != nullptr && set < result->dir_names_num) {
std::snprintf(result->dir_names[set].data, sizeof(result->dir_names[set].data), "%s", name.c_str());
if (result->params != nullptr) {
result->params[set] = load_param(entry.path().string());
}
set++;
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);
}
}
result->hit_num = hit;
result->hit_num = static_cast<std::uint32_t>(matches.size());
result->set_num = set;
return SAVE_DATA_OK;
}

View File

@ -33,6 +33,14 @@ constexpr std::uint32_t SAVE_DATA_PARAM_TYPE_DETAIL = 3;
constexpr std::uint32_t SAVE_DATA_PARAM_TYPE_USER_PARAM = 4;
constexpr std::uint32_t SAVE_DATA_PARAM_TYPE_MTIME = 5;
constexpr std::uint32_t SAVE_DATA_SORT_KEY_DIRNAME = 0;
constexpr std::uint32_t SAVE_DATA_SORT_KEY_USER_PARAM = 1;
constexpr std::uint32_t SAVE_DATA_SORT_KEY_BLOCKS = 2;
constexpr std::uint32_t SAVE_DATA_SORT_KEY_MTIME = 3;
constexpr std::uint32_t SAVE_DATA_SORT_KEY_FREE_BLOCKS = 5;
constexpr std::uint32_t SAVE_DATA_SORT_ORDER_ASCENT = 0;
constexpr std::uint32_t SAVE_DATA_SORT_ORDER_DESCENT = 1;
constexpr std::size_t SAVE_DATA_MOUNT_SLOTS = 16;
struct MountSlot {

View File

@ -8,17 +8,79 @@
#include <random>
#include <stdexcept>
#include <string>
#include <vector>
extern "C" {
int APS5_VABI sceSaveDataDirNameSearch(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<std::string> 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<SceSaveDataDirName, 3> 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<std::string> 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<Save, 3> 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::string>;
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");
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 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);
@ -64,6 +126,7 @@ int main() {
int status = 0;
try {
SearchAndMount();
SortByKey();
} catch (const std::exception& error) {
std::fprintf(stderr, "%s\n", error.what());
status = 1;

View File

@ -302,6 +302,7 @@ Throughout the project, every function at every stage either **does exactly what
- [sceNgs2VoiceQueryInfo](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - only info id 0x4001 is known: PPSA22520 passes an 8-byte buffer and branches on the first word being 1 or 2, so it is assumed to be the voice channel count (0 before the voice is set up); the second word is zeroed and other ids throw
- [sceNgs2VoiceGetPortInfo](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - layout from shadPS4; the delay and destination input are zero because ports have neither yet
- [sceNgs2ParseWaveformFile](../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - the signature `(path, offset, info)` is taken from the call site of PPSA21402, which sets only three argument registers; the file is read from `offset` to its end, the data and block offsets are taken to be relative to the start of the file, not of `offset`, and an unreadable file throws. The audio unit fields of a 16-bit PCM waveform are assumed to be one sample frame per unit and per frame; `INVALID_WAVEFORM_DATA` for a `data` chunk that runs past the buffer or ends inside a frame is assumed, and looped PCM and other PCM formats throw
- [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. 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
- [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
- [sceTextToSpeech2GetSystemStatus](../../core/libs/prx/libSceTextToSpeech2/Export.cpp) (libSceTextToSpeech2) - unknown signature; returns `EOPNOTSUPP` like `sceTextToSpeech2Initialize`
- [sceCoredumpAttachUserMemoryFile](../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - unknown signature