From 12ef40e4f1d23be05872f47c1ea1dbbd9b390611 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+yunusshaikh7@users.noreply.github.com> Date: Mon, 12 Oct 2026 06:12:47 -0700 Subject: [PATCH] fix(savedata): let a trailing % in a directory name pattern match nothing sceSaveDataDirNameSearch reads % in the directory name pattern as any run of characters and _ as one character. The matcher stopped when the name ran out and then required the pattern to have ended too, so a trailing % with nothing left to match failed: SAVEDATA% did not find a save named SAVEDATA, and TRAI_% did not find TRAIL. A % now matches no characters at the end of a name as it already did in the middle. --- .../libs/prx/libSceSaveData.native/Export.cpp | 3 +++ core/libs/tests/GuestSaveDataSearch.cpp | 26 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 29494d4f1..b09e7deaa 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -62,6 +62,9 @@ static bool dir_name_match(const char* str, const char* pattern) { str++; pattern++; } + while (*pattern == '%') { + pattern++; + } return *str == '\0' && *pattern == '\0'; } diff --git a/core/libs/tests/GuestSaveDataSearch.cpp b/core/libs/tests/GuestSaveDataSearch.cpp index 601783fe8..9cc13cdee 100644 --- a/core/libs/tests/GuestSaveDataSearch.cpp +++ b/core/libs/tests/GuestSaveDataSearch.cpp @@ -81,6 +81,31 @@ static void SortByKey() { 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); @@ -139,6 +164,7 @@ int main() { try { SearchAndMount(); SortByKey(); + TrailingWildcards(); } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); status = 1; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index e05b39391..b5577c263 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -328,7 +328,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. 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 +- [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 - [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