Merge pull request #8285 from DealPotato/feat/savedata-mount2

feat(savedata): add sceSaveDataMount2
This commit is contained in:
Dean authored and GitHub committed 2026-11-02 03:22:25 +13:00
commit d94fad17de
6 files changed
+129 -2

No files matched your search

+10
View File
@@ -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;
@@ -18,6 +18,7 @@
#include <filesystem>
#include <stdexcept>
#include <string>
#include <string_view>
#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<int>(sizeof(SceSaveDataDirName::data)), mount != nullptr && mount->dir_name != nullptr ? mount->dir_name->data : "(null)", mount != nullptr ? mount->mount_mode : 0u, mount != nullptr ? static_cast<unsigned long long>(mount->blocks) : 0ull, static_cast<unsigned>(rc), static_cast<int>(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;
@@ -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;
@@ -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);
+80 -2
View File
@@ -2,6 +2,7 @@
#include "prx/libSceSaveData.native/include/Api.hpp"
#include "prx/libc/include/general/VabiMacros.hpp"
#include "SceTypes.hpp"
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <filesystem>
@@ -10,13 +11,19 @@
#include <stdexcept>
#include <string>
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;
+1
View File
@@ -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.