diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 36d3d47ec..4269cee09 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1571,6 +1571,13 @@ struct SaveDataBackup { std::uint8_t reserved[32]; }; +struct SaveDataConvert { + std::int32_t user_id; + std::int32_t pad; + const SceSaveDataDirName* dir_name; + std::uint8_t unknown[48]; +}; + struct AppContentInitParam { char reserved[32]; }; struct AppContentBootParam { diff --git a/core/libs/prx/libSceSaveData.native/CMakeLists.txt b/core/libs/prx/libSceSaveData.native/CMakeLists.txt index 3b596955e..6b03a2c86 100644 --- a/core/libs/prx/libSceSaveData.native/CMakeLists.txt +++ b/core/libs/prx/libSceSaveData.native/CMakeLists.txt @@ -22,6 +22,15 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_savedata_convert_tests ${LIBS_INCLUDE_DIR}/tests/GuestSaveDataConvert.cpp) + target_include_directories(guest_savedata_convert_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_savedata_convert_tests PRIVATE libSceSaveData.native libc) + add_test(NAME guest_savedata_convert COMMAND guest_savedata_convert_tests) + if(WIN32) + set_tests_properties(guest_savedata_convert PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_savedata_mount_status_tests ${LIBS_INCLUDE_DIR}/tests/GuestSaveDataMountStatus.cpp) target_include_directories(guest_savedata_mount_status_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_savedata_mount_status_tests PRIVATE libSceSaveData.native libc) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index a4475f9f4..89fd8a121 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -762,9 +762,28 @@ int APS5_VABI sceSaveDataDirNameSearchPs4(const SaveDataDirNameSearchCond* cond, return rc; } -int APS5_VABI sceSaveDataConvert() { - NotImplemented_nid_no_patch(__func__); - return 0; +static int convert(const SaveDataConvert* param) { + if (param == nullptr || param->dir_name == nullptr) { + throw std::runtime_error("sceSaveDataConvert: null argument"); + } + const auto* nameEnd = static_cast(std::memchr(param->dir_name->data, '\0', sizeof(param->dir_name->data))); + if (nameEnd == nullptr) { + return SAVE_DATA_ERROR_PARAMETER; + } + const std::string dirName(param->dir_name->data, static_cast(nameEnd - param->dir_name->data)); + if (dirName.empty() || dirName == "." || dirName == ".." || dirName.find_first_of("/\\:") != std::string::npos) { + return SAVE_DATA_ERROR_PARAMETER; + } + if (!std::filesystem::is_directory(save_root() + "/" + dirName)) { + return SAVE_DATA_ERROR_NOT_FOUND; + } + return SAVE_DATA_OK; +} + +int APS5_VABI sceSaveDataConvert(const SaveDataConvert* param) { + const int rc = convert(param); + SAVEDATA_TRACE("convert user=%d dir=%s -> 0x%08x", param != nullptr ? param->user_id : -1, param != nullptr && param->dir_name != nullptr ? param->dir_name->data : "(null)", static_cast(rc)); + return rc; } int APS5_VABI sceSaveDataGetConvertProgress() { diff --git a/core/libs/tests/GuestSaveDataConvert.cpp b/core/libs/tests/GuestSaveDataConvert.cpp new file mode 100644 index 000000000..8df9b84da --- /dev/null +++ b/core/libs/tests/GuestSaveDataConvert.cpp @@ -0,0 +1,73 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceSaveDataConvert(const SaveDataConvert*); +int APS5_VABI sceSaveDataInitialize3(const void*); +} + +namespace { + +constexpr int SaveDataErrorParameter = -2137063424; +constexpr int SaveDataErrorNotFound = -2137063416; + +int failures = 0; + +void Check(bool condition, const std::string& what) { + if (condition) return; + std::fprintf(stderr, "savedata convert check failed: %s\n", what.c_str()); + ++failures; +} + +int Convert(const char* data, std::size_t size) { + SceSaveDataDirName name{}; + std::memcpy(name.data, data, size); + SaveDataConvert convert{}; + convert.user_id = 0x10000000; + convert.dir_name = &name; + return sceSaveDataConvert(&convert); +} + +std::vector Read(const std::filesystem::path& path) { + std::ifstream stream(path, std::ios::binary); + return {std::istreambuf_iterator(stream), std::istreambuf_iterator()}; +} + +} + +int main() { + const auto root = std::filesystem::temp_directory_path() / ("anyps5-savedata-convert-" + std::to_string(std::random_device{}())); + const auto work = root / "work"; + const auto kept = work / "_sd" / "kept" / "data.bin"; + std::filesystem::create_directories(kept.parent_path()); + std::ofstream(kept) << "data"; + const auto previous = std::filesystem::current_path(); + std::filesystem::current_path(work); + + Check(sceSaveDataInitialize3(nullptr) == 0, "SaveData initializes"); + Check(Convert("kept", sizeof("kept")) == 0, "an existing save converts"); + Check(Read(kept) == std::vector{'d', 'a', 't', 'a'}, "the converted save keeps its files"); + Check(Convert("missing", sizeof("missing")) == SaveDataErrorNotFound, "a missing save is not found"); + for (const char* invalid : {"../kept", "", ".", "..", "a\\b", "c:d"}) { + Check(Convert(invalid, std::strlen(invalid) + 1) == SaveDataErrorParameter, std::string("\"") + invalid + "\" is rejected"); + } + char unterminated[sizeof(SceSaveDataDirName::data)]; + std::memset(unterminated, 'a', sizeof(unterminated)); + Check(Convert(unterminated, sizeof(unterminated)) == SaveDataErrorParameter, "an unterminated name is rejected"); + + std::filesystem::current_path(previous); + std::error_code error; + std::filesystem::remove_all(root, error); + if (failures != 0) return 1; + std::printf("savedata convert tests passed\n"); + return 0; +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 83fd2ced4..59ccac655 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -136,7 +136,8 @@ Throughout the project, every function at every stage either **does exactly what - [sceFontWritingRefersRenderStepCharacter](../../core/libs/prx/libSceFont/src/Text.cpp) (libSceFont) - the second argument is an out pointer to the writing's letter step, as in [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1882-L1896) and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings. Measured on PS4 firmware 11.00 `libSceFont.sprx` (not on a PS5): exactly 8 bytes are written, the address of a step stored inside the writing, and a null writing writes null; shadPS4 and prosper treat the argument as a struct, which the measurement contradicts. The letter step lives in the writing, so the pointer stays valid as long as the writing does, and its contents are rebuilt by each call of this function. On the PS4 library repeated calls on one step walk the characters it covers and then return null; here every call returns the step's one character, so the second call differs. Assumed: each step covers one character and one glyph. PPSA06699 (Hot Shots Golf 2) dereferences the returned pointer for its character counts. - [sceFontWritingLineGetOrderingSpace](../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - the four outputs are written in the order the PS4 11.00 `libSceFont.sprx` writes them (line advance, leading space, space between spaced runs, trailing space); the parameter names in the [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) IDA type database and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings order them differently (head, inline, tail, advance), and the PS5 library was not measured. PPSA01325's only call uses them in the PS4 order: it computes first - second - fourth - 0.25 * third and compares that, divided by a scale, with its width limit. The PS4 library writes the trailing space through its pointer without a null check whenever the leading-space pointer is set; that call throws here - [sceSaveDataTransferringMountPs4, sceSaveDataDirNameSearchPs4](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - arguments assumed to be those of `sceSaveDataTransferringMount` and `sceSaveDataDirNameSearch`: KytyPS5 binds both NIDs to the PS5 functions, and [prosper](https://github.com/mattias800/prosper) captured the same result layout on a title for the search. There is no PS4 save area, so the mount returns `NOT_FOUND` and the search reports zero hits -- [sceCoredumpWriteUserString](../../core/libs/prx/libkernel/System/src/Coredump.cpp) and [sceSaveDataConvert, sceSaveDataGetConvertProgress](../../core/libs/prx/libSceSaveData.native/Export.cpp) - imported by PPSA16064, not called during its start-up and added without an implementation; unknown signatures +- [sceCoredumpWriteUserString](../../core/libs/prx/libkernel/System/src/Coredump.cpp) and [sceSaveDataGetConvertProgress](../../core/libs/prx/libSceSaveData.native/Export.cpp) - imported by PPSA16064, not called during its start-up and added without an implementation; unknown signatures +- [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 - [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 - [sceAudioOutExClose, sceAudioOutExConfigureOutput, sceAudioOutExGetMonitorInfo, sceAudioOutExOpen](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA99505, which fails to load without them; added without an implementation and declared without parameters. PPSA99505 calls them only for its HDMI bitstream output, an option that is off by default, to read the formats the TV or receiver lists, switch the HDMI output mode and open a port for IEC 61937 bursts ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L110-L179)); [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/da0c0451333bab82e79fffd2de4718e917f9dcb0/projects/evoplayer/core/src/audio/evo_pt.cpp) also calls `sceAudioOutExOpen`, `sceAudioOutExConfigureOutput` and `sceAudioOutExClose` for HDMI bitstream output. The two declare the arguments of `sceAudioOutExConfigureOutput` differently and neither is confirmed on a console. Audio reaches the host device through SDL as PCM, so there is no encoded HDMI output to switch to - [sceOpusCeltDecCreateEx, sceOpusCeltDecDecode, sceOpusCeltDecDestroy, sceOpusCeltDecGetSize, sceOpusCeltDecInitialize, sceOpusCeltDecTerminate](../../core/libs/prx/libSceOpusCeltDec/Export.cpp) (libSceOpusCeltDec) - imported by PPSA99001, which fails to load without the module; added without an implementation. Their signatures match libSceOpusDec's in [ProsperoRadio](https://github.com/blackbearreloaded/ProsperoRadio/blob/2c4029c3a8f43e9e1dfef98374d94d99b14d745d/src/opus_decoder.cpp). PPSA99001 opens this decoder only as a fallback when libSceOpusDec reports a retryable error for a CELT packet, which [libSceOpusDec](../../core/libs/prx/libSceOpusDec/Export.cpp) here never returns, so the fallback is not reached