diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 2290a2a3b3..6bc89e4df9 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1711,6 +1711,16 @@ struct SaveDataMount3 { std::uint8_t reserved[32]; }; +struct SaveDataMount2 { + int user_id; + int pad; + const SceSaveDataDirName* dir_name; + std::uint64_t blocks; + std::uint32_t mount_mode; + std::uint8_t reserved[32]; + int pad2; +}; + struct SaveDataDirNameSearchCond { std::int32_t user_id; std::int32_t pad; diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 644b421f49..db0f405d06 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestArena.hpp" @@ -757,6 +758,40 @@ int APS5_VABI sceSaveDataMount3(const SaveDataMount3* mount, SaveDataMountResult return rc; } +static int mount2(const SaveDataMount2* mount, SaveDataMountResult* mount_result) { + if (mount == nullptr || mount_result == nullptr) { + return SAVE_DATA_ERROR_PARAMETER; + } + if (g_initializations == 0) { + return SAVE_DATA_ERROR_NOT_INITIALIZED; + } + if (mount->user_id == -1 || mount->user_id == 0) { + return SAVE_DATA_ERROR_INVALID_LOGIN_USER; + } + const bool creates = (mount->mount_mode & SAVE_DATA_MOUNT_MODE_CREATE) != 0; + if (mount->mount_mode == 0 || (creates && mount->blocks < SAVE_DATA_BLOCKS_MIN2)) { + return SAVE_DATA_ERROR_PARAMETER; + } + if (mount->dir_name != nullptr) { + const std::string_view name(mount->dir_name->data, sizeof(mount->dir_name->data)); + if (name.substr(0, name.find('\0')).find('_') != std::string_view::npos) { + return SAVE_DATA_ERROR_PARAMETER; + } + } + SaveDataMount3 converted{}; + converted.user_id = mount->user_id; + converted.dir_name = mount->dir_name; + converted.blocks = mount->blocks; + converted.mount_mode = mount->mount_mode; + return mount3(&converted, mount_result); +} + +int APS5_VABI sceSaveDataMount2(const SaveDataMount2* mount, SaveDataMountResult* mount_result) { + const int rc = mount2(mount, mount_result); + SAVEDATA_TRACE("mount2 user=%d dir=%.*s mode=0x%x blocks=%llu -> 0x%08x point=%.*s status=%u", mount != nullptr ? mount->user_id : -1, static_cast(sizeof(SceSaveDataDirName::data)), mount != nullptr && mount->dir_name != nullptr ? mount->dir_name->data : "(null)", mount != nullptr ? mount->mount_mode : 0u, mount != nullptr ? static_cast(mount->blocks) : 0ull, static_cast(rc), static_cast(sizeof(SaveDataMountPoint::data)), mount_result != nullptr ? mount_result->mount_point.data : "", mount_result != nullptr ? mount_result->mount_status : 0u); + return rc; +} + int APS5_VABI sceSaveDataPrepare(const SaveDataMountPoint* mount_point, const SaveDataPrepareParam* param) { (void)mount_point; (void)param; diff --git a/core/libs/prx/libSceSaveData.native/SaveData.hpp b/core/libs/prx/libSceSaveData.native/SaveData.hpp index b8d2b8a1f9..0dc846f6e5 100644 --- a/core/libs/prx/libSceSaveData.native/SaveData.hpp +++ b/core/libs/prx/libSceSaveData.native/SaveData.hpp @@ -19,6 +19,7 @@ constexpr int SAVE_DATA_ERROR_EXISTS = -2137063417; constexpr int SAVE_DATA_ERROR_NOT_FOUND = -2137063416; constexpr int SAVE_DATA_ERROR_NO_SPACE_FS = -2137063414; constexpr int SAVE_DATA_ERROR_INTERNAL = -2137063413; // 0x809F000B +constexpr int SAVE_DATA_ERROR_INVALID_LOGIN_USER = -2137063407; // 0x809F0011 constexpr int SAVE_DATA_ERROR_MEMORY_NOT_READY = -2137063406; // 0x809F0012 constexpr std::uint32_t SAVE_DATA_MOUNT_MODE_RDONLY = 1; @@ -26,6 +27,7 @@ constexpr std::uint32_t SAVE_DATA_MOUNT_MODE_RDWR = 2; constexpr std::uint32_t SAVE_DATA_MOUNT_MODE_CREATE = 4; constexpr std::uint32_t SAVE_DATA_MOUNT_MODE_CREATE2 = 32; +constexpr std::uint64_t SAVE_DATA_BLOCKS_MIN2 = 96; constexpr std::uint64_t SAVE_DATA_BLOCKS_MAX = 16384; constexpr std::uint32_t SAVE_DATA_PARAM_TYPE_ALL = 0; constexpr std::uint32_t SAVE_DATA_PARAM_TYPE_TITLE = 1; diff --git a/core/libs/prx/libSceSaveData.native/include/Api.hpp b/core/libs/prx/libSceSaveData.native/include/Api.hpp index 00283d8bfd..78eaadc8c4 100644 --- a/core/libs/prx/libSceSaveData.native/include/Api.hpp +++ b/core/libs/prx/libSceSaveData.native/include/Api.hpp @@ -24,6 +24,7 @@ int APS5_VABI sceSaveDataGetMountInfo(const SaveDataMountPoint* mountPoint, Save int APS5_VABI sceSaveDataGetParam(const SaveDataMountPoint* mountPoint, uint32_t paramType, void* paramBuf, size_t paramBufSize, size_t* gotSize); int APS5_VABI sceSaveDataLoadIcon(const SaveDataMountPoint* mountPoint, SaveDataIcon* icon); int APS5_VABI sceSaveDataMount3(const SaveDataMount3* mount, SaveDataMountResult* mountResult); +int APS5_VABI sceSaveDataMount2(const SaveDataMount2* mount, SaveDataMountResult* mountResult); int APS5_VABI sceSaveDataPrepare(const SaveDataMountPoint* mountPoint, const SaveDataPrepareParam* param); int APS5_VABI sceSaveDataSaveIcon(const SaveDataMountPoint* mountPoint, const SaveDataIcon* icon); int APS5_VABI sceSaveDataSaveIconByPath(const SaveDataMountPoint* mountPoint, const char* path); diff --git a/core/libs/tests/GuestSaveDataPs4.cpp b/core/libs/tests/GuestSaveDataPs4.cpp index d35c27146d..2aec1faa9f 100644 --- a/core/libs/tests/GuestSaveDataPs4.cpp +++ b/core/libs/tests/GuestSaveDataPs4.cpp @@ -2,6 +2,7 @@ #include "prx/libSceSaveData.native/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "SceTypes.hpp" +#include #include #include #include @@ -10,13 +11,19 @@ #include #include +static_assert(offsetof(SaveDataMount2, dir_name) == 8 && offsetof(SaveDataMount2, blocks) == 16 && offsetof(SaveDataMount2, mount_mode) == 24 && sizeof(SaveDataMount2) == 64, "SaveDataMount2 has the PS4 layout"); + namespace { -constexpr int SaveDataErrorNotFound = -2137063416; -constexpr int SaveDataErrorNotInitialized = -2137063423; constexpr int SaveDataErrorParameter = -2137063424; +constexpr int SaveDataErrorNotInitialized = -2137063423; +constexpr int SaveDataErrorNotFound = -2137063416; constexpr int SaveDataErrorNotMounted = -2137063420; +constexpr int SaveDataErrorBusy = -2137063421; +constexpr int SaveDataErrorInvalidLoginUser = -2137063407; constexpr std::uint32_t MountReadWrite = 2; +constexpr std::uint32_t MountCreate = 4; +constexpr std::uint32_t MountCreate2 = 32; int failures = 0; @@ -71,6 +78,76 @@ void TestDirNameSearchPs4() { Check(sceSaveDataDirNameSearchPs4(&cond, nullptr) == SaveDataErrorParameter, "null result returns PARAMETER in sceSaveDataDirNameSearchPs4"); } +void TestMount2() { + SceSaveDataDirName created{}; + std::strcpy(created.data, "mount2-new"); + SaveDataMount2 mount{}; + mount.user_id = 1; + mount.dir_name = &created; + mount.blocks = 96; + mount.mount_mode = MountReadWrite | MountCreate2; + SaveDataMountResult result{}; + Check(sceSaveDataMount2(&mount, &result) == SaveDataErrorNotInitialized, "Mount2 before initialization is NOT_INITIALIZED"); + Check(sceSaveDataInitialize3(nullptr) == 0, "SaveData initializes"); + Check(sceSaveDataMount2(&mount, &result) == 0, "Mount2 creates a missing save"); + Check(result.mount_status == 1u, "Mount2 reports the save as created"); + Check(result.mount_point.data[0] != '\0', "Mount2 reports a mount point"); + Check(std::filesystem::is_directory(std::filesystem::path("_sd") / "mount2-new"), "Mount2 creates the save directory"); + SaveDataMountResult busy{}; + Check(sceSaveDataMount2(&mount, &busy) == SaveDataErrorBusy, "Mount2 of a mounted save is BUSY"); + Check(sceSaveDataUmount2(0, &result.mount_point) == 0, "the Mount2 mount point unmounts"); + + SceSaveDataDirName kept{}; + std::strcpy(kept.data, "kept"); + mount.dir_name = &kept; + mount.mount_mode = MountReadWrite; + Check(sceSaveDataMount2(&mount, &result) == 0, "Mount2 opens an existing save"); + Check(result.mount_status == 0u, "Mount2 reports the existing save as opened"); + Check(sceSaveDataUmount2(0, &result.mount_point) == 0, "the opened save unmounts"); + mount.blocks = 0; + mount.mount_mode = MountReadWrite | MountCreate2; + Check(sceSaveDataMount2(&mount, &result) == 0, "Mount2 CREATE2 with 0 blocks opens an existing save"); + Check(result.mount_status == 0u, "Mount2 CREATE2 reports the existing save as opened"); + Check(sceSaveDataUmount2(0, &result.mount_point) == 0, "the CREATE2 mount point unmounts"); + mount.blocks = 96; + mount.mount_mode = MountReadWrite; + + SceSaveDataDirName absent{}; + std::strcpy(absent.data, "absent"); + mount.dir_name = &absent; + Check(sceSaveDataMount2(&mount, &result) == SaveDataErrorNotFound, "Mount2 open of a missing save is NOT_FOUND"); + Check(sceSaveDataMount2(nullptr, &result) == SaveDataErrorParameter, "Mount2 with a null mount is PARAMETER"); + Check(sceSaveDataMount2(&mount, nullptr) == SaveDataErrorParameter, "Mount2 with a null result is PARAMETER"); + + SceSaveDataDirName small{}; + std::strcpy(small.data, "mount2-small"); + mount.dir_name = &small; + mount.mount_mode = 0; + Check(sceSaveDataMount2(&mount, &result) == SaveDataErrorParameter, "Mount2 with mount mode 0 is PARAMETER"); + mount.blocks = 1; + mount.mount_mode = MountReadWrite | MountCreate; + Check(sceSaveDataMount2(&mount, &result) == SaveDataErrorParameter, "Mount2 creating a save of fewer than 96 blocks is PARAMETER"); + Check(!std::filesystem::exists(std::filesystem::path("_sd") / "mount2-small"), "a refused Mount2 creates no directory"); + + mount.blocks = 96; + for (const char* name : {"survey_test", "SURVEY_TEST"}) { + SceSaveDataDirName underscored{}; + std::strcpy(underscored.data, name); + mount.dir_name = &underscored; + Check(sceSaveDataMount2(&mount, &result) == SaveDataErrorParameter, std::string("Mount2 of ") + name + " is PARAMETER"); + Check(!std::filesystem::exists(std::filesystem::path("_sd") / name), std::string("a refused Mount2 of ") + name + " creates no directory"); + } + SceSaveDataDirName mixed{}; + std::strcpy(mixed.data, "surveyTestAuto0"); + mount.dir_name = &mixed; + Check(sceSaveDataMount2(&mount, &result) == 0, "Mount2 creates a mixed-case save"); + Check(sceSaveDataUmount2(0, &result.mount_point) == 0, "the mixed-case save unmounts"); + for (const int user : {-1, 0}) { + mount.user_id = user; + Check(sceSaveDataMount2(&mount, &result) == SaveDataErrorInvalidLoginUser, "Mount2 of user " + std::to_string(user) + " is INVALID_LOGIN_USER"); + } +} + void TestSaveDataMemoryV1() { Check(sceSaveDataSetupSaveDataMemory(7, 0x1000, nullptr) == SaveDataErrorNotInitialized, "v1 SetupSaveDataMemory before v1 Initialize"); Check(sceSaveDataInitialize(nullptr) == 0, "v1 Initialize"); @@ -116,6 +193,7 @@ int main() { TestDirNameSearchPs4(); TestSaveDataMemoryV1(); TestMountPs4(); + TestMount2(); std::filesystem::current_path(previous); std::error_code error; diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index c0256b7758..f8b9aac4fb 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -27,6 +27,7 @@ - [sceSaveDataDirNameSearch, sceSaveDataDirNameSearch2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - only the running title's saves are kept, so a search with another title's `title_id` reports zero hits, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/ec334904604ba41f5d4753f4a2498ec82540e5d6/src/core/libraries/save_data/savedata.cpp#L791-L802) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L592-L596) do when that title has no save directory; what a console returns for another title's saves is not verified. The running title's ID is the `titleId` in its `param.json` - [sceSaveDataDirNameSearch2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) runs the same search as `sceSaveDataDirNameSearch` with the same condition and result structures, as [SharpEmu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/SaveData/SaveDataExports.cs) does, its only source; what distinguishes it on the console is unknown. One title (PPSA35669, in a plugin) disassembled for this entry has one call site (0x2eda71); it clears 56 bytes for the result structure first (the size of the structure used here), puts a value returned by a call with the argument 0x8000 at offset 8 and 0x400 at offset 0x10, and afterwards branches on a negative result and reads the 32-bit field at offset 0, skipping its loop when it is 0. That agrees with the result structure of `sceSaveDataDirNameSearch`. The condition structure was not read. +- [sceSaveDataMount2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - answers as a PS4 Pro (firmware 10.71) did in [#8285](https://github.com/boykopovar/AnyPS5/pull/8285): `SAVE_DATA_ERROR_NOT_INITIALIZED` before `sceSaveDataInitialize3`, `SAVE_DATA_ERROR_INVALID_LOGIN_USER` for user IDs -1 and 0, and `SAVE_DATA_ERROR_PARAMETER` for mount mode 0, for a CREATE mount of fewer than 96 blocks and for a directory name containing `_` (in either case; `surveytest` and `surveyTestAuto0` mount). The 96-block minimum is applied to CREATE only, since CREATE2 was not measured, and the `_` rule was measured with CREATE mounts only. Other user IDs are not checked against the signed-in users. None of these rows were measured on a PS5 for `sceSaveDataMount3` - [sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a failed mount (BUSY, EXISTS, NOT_FOUND, MOUNT_FULL) leaves the result as it was, as shadPS4 does; not checked on hardware. Animal Well (PPSA22520) depends on it: it checks for its save with a mount it never unmounts, then saves with a second mount of the same directory, ignores its BUSY and keeps using the first mount point for `sceSaveDataPrepare`, its file and `sceSaveDataUmount2`. In PPSA05347 the function that calls it at 0x235fbb5 zeroes its 0x40-byte result before the call and, for a negative result other than 0x809f0008 and 0x809f000f (BUSY included), logs the code and returns it without recording the mount; no abort follows. - [sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - an unterminated or empty directory name, `.`, `..`, or a name containing `/`, `\` or `:` returns `SAVE_DATA_ERROR_PARAMETER`, which 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-mount3.md#L196-L204) lists for an invalid parameter, and leaves the result as it was, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L652-L658) does. The [SDK](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-dir-name.md#L32-L36) lets an application use only letters, digits, `-`, `.` and `@` in a name, and KytyPS5 also accepts `_` ([`valid_path_component`](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L276-L287)); which other names the console's mount rejects is not checked on hardware - [sceSaveDataBackup](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - an unterminated directory name returns `SAVE_DATA_ERROR_PARAMETER` to prevent unbounded copy; which other directory names the console's backup rejects is unverified on hardware.