diff --git a/core/libs/prx/libSceAppContent/Export.cpp b/core/libs/prx/libSceAppContent/Export.cpp index 4d0b5e3d5..4620e46f3 100644 --- a/core/libs/prx/libSceAppContent/Export.cpp +++ b/core/libs/prx/libSceAppContent/Export.cpp @@ -6,10 +6,13 @@ #include #include #include +#include #include #include static constexpr int SCE_APP_CONTENT_ERROR_PARAMETER = static_cast(0x80D90002); +static constexpr int SCE_APP_CONTENT_ERROR_BUSY = static_cast(0x80D90003); +static constexpr int SCE_APP_CONTENT_ERROR_NOT_MOUNTED = static_cast(0x80D90004); static constexpr int SCE_APP_CONTENT_ERROR_NOT_FOUND = static_cast(0x80D90005); static constexpr int SCE_APP_CONTENT_ERROR_DRM_NO_ENTITLEMENT = static_cast(0x80D90007); static constexpr uint32_t APPPARAM_ID_SKU_FLAG = 0; @@ -24,9 +27,25 @@ struct AppContentAddcontInfo { uint32_t status; }; -static std::filesystem::path TemporaryDirectory(const AppContentMountPoint* mount_point) { - if (!mount_point || std::strncmp(mount_point->data, TEMPORARY_MOUNT_POINT, sizeof(mount_point->data)) != 0) APS5_INVALID_ARG_EX; - return ResolvePath_nid_no_patch(TEMPORARY_MOUNT_POINT); +namespace { + +struct TemporaryData { + std::mutex mutex; + const std::filesystem::path directory = ResolvePath_nid_no_patch(TEMPORARY_MOUNT_POINT); + bool mounted = false; + + TemporaryData() { BlockPathAlias_nid_no_patch(TEMPORARY_MOUNT_POINT); } +}; + +TemporaryData& Temporary() { + static TemporaryData state; + return state; +} + +bool IsTemporaryMountPoint(const AppContentMountPoint* mountPoint) { + return std::strncmp(mountPoint->data, TEMPORARY_MOUNT_POINT, sizeof(mountPoint->data)) == 0; +} + } static void ClearDirectory(const std::filesystem::path& directory) { @@ -96,29 +115,50 @@ int APS5_VABI sceAppContentDownloadDataGetAvailableSpaceKb(const AppContentMount int APS5_VABI sceAppContentInitialize(const AppContentInitParam* init_param, AppContentBootParam* boot_param) { (void)init_param; if (!boot_param) return SCE_APP_CONTENT_ERROR_PARAMETER; + Temporary(); std::memset(boot_param, 0, sizeof(*boot_param)); return 0; } -int APS5_VABI sceAppContentTemporaryDataFormat(const AppContentMountPoint* mount_point) { - ClearDirectory(TemporaryDirectory(mount_point)); +int APS5_VABI sceAppContentTemporaryDataFormat(const AppContentMountPoint* mountPoint) { + if (!mountPoint) return SCE_APP_CONTENT_ERROR_PARAMETER; + auto& state = Temporary(); + std::lock_guard lock(state.mutex); + if (!state.mounted || !IsTemporaryMountPoint(mountPoint)) return SCE_APP_CONTENT_ERROR_NOT_MOUNTED; + ClearDirectory(state.directory); return 0; } -int APS5_VABI sceAppContentTemporaryDataGetAvailableSpaceKb(const AppContentMountPoint* mount_point, size_t* available_space_kb) { - if (!available_space_kb) return SCE_APP_CONTENT_ERROR_PARAMETER; - *available_space_kb = static_cast(std::filesystem::space(TemporaryDirectory(mount_point)).available / 1024); +int APS5_VABI sceAppContentTemporaryDataGetAvailableSpaceKb(const AppContentMountPoint* mountPoint, size_t* availableSpaceKb) { + if (!mountPoint || !availableSpaceKb) return SCE_APP_CONTENT_ERROR_PARAMETER; + auto& state = Temporary(); + std::lock_guard lock(state.mutex); + if (!state.mounted || !IsTemporaryMountPoint(mountPoint)) return SCE_APP_CONTENT_ERROR_NOT_MOUNTED; + *availableSpaceKb = static_cast(std::filesystem::space(state.directory).available / 1024); return 0; } -int APS5_VABI sceAppContentTemporaryDataMount2(uint32_t option, AppContentMountPoint* mount_point) { - if (!mount_point) return SCE_APP_CONTENT_ERROR_PARAMETER; - if (option > TEMPORARY_DATA_OPTION_FORMAT) throw std::invalid_argument(std::string(__func__) + ": unknown option " + std::to_string(option)); - std::memset(mount_point->data, 0, sizeof(mount_point->data)); - std::memcpy(mount_point->data, TEMPORARY_MOUNT_POINT, sizeof(TEMPORARY_MOUNT_POINT)); - const auto directory = TemporaryDirectory(mount_point); - if (std::filesystem::create_directories(directory)) RecordWrittenPath_nid_no_patch(directory); - if (option == TEMPORARY_DATA_OPTION_FORMAT) ClearDirectory(directory); +int APS5_VABI sceAppContentTemporaryDataMount2(uint32_t option, AppContentMountPoint* mountPoint) { + if (!mountPoint || option > TEMPORARY_DATA_OPTION_FORMAT) return SCE_APP_CONTENT_ERROR_PARAMETER; + auto& state = Temporary(); + std::lock_guard lock(state.mutex); + if (state.mounted) return SCE_APP_CONTENT_ERROR_BUSY; + if (std::filesystem::create_directories(state.directory)) RecordWrittenPath_nid_no_patch(state.directory); + if (option == TEMPORARY_DATA_OPTION_FORMAT) ClearDirectory(state.directory); + AddPathAlias_nid_no_patch(TEMPORARY_MOUNT_POINT, state.directory.string().c_str()); + state.mounted = true; + std::memset(mountPoint->data, 0, sizeof(mountPoint->data)); + std::memcpy(mountPoint->data, TEMPORARY_MOUNT_POINT, sizeof(TEMPORARY_MOUNT_POINT)); + return 0; +} + +int APS5_VABI sceAppContentTemporaryDataUnmount(const AppContentMountPoint* mountPoint) { + if (!mountPoint) return SCE_APP_CONTENT_ERROR_PARAMETER; + auto& state = Temporary(); + std::lock_guard lock(state.mutex); + if (!state.mounted || !IsTemporaryMountPoint(mountPoint)) return SCE_APP_CONTENT_ERROR_NOT_MOUNTED; + BlockPathAlias_nid_no_patch(TEMPORARY_MOUNT_POINT); + state.mounted = false; return 0; } diff --git a/core/libs/prx/libc/include/General.hpp b/core/libs/prx/libc/include/General.hpp index 6f696024e..3aa9dcbc6 100644 --- a/core/libs/prx/libc/include/General.hpp +++ b/core/libs/prx/libc/include/General.hpp @@ -20,6 +20,7 @@ extern "C" void RemovePathAlias_nid_no_patch(const char* guestPrefix); extern "C" void RecordWrittenPath_nid_no_patch(const std::filesystem::path& path); extern "C" std::vector WrittenPaths_nid_no_patch(); extern "C" void SyncWrittenPaths_nid_no_patch(); +extern "C" void BlockPathAlias_nid_no_patch(const char* guestPrefix); #define APS5_INVALID_ARG_EX throw std::invalid_argument(std::string(__func__) + ": invalid argument") diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 38eb37efb..c0ca6ccc2 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -31,6 +31,7 @@ namespace { struct PathAliases { std::mutex mutex; std::vector> entries; + std::vector blocked; }; PathAliases& Aliases() { @@ -55,6 +56,10 @@ std::optional ResolveAlias(const std::string& guestPath) const auto relative = TrimSlashes(guestPath.c_str()); auto& aliases = Aliases(); std::lock_guard lock(aliases.mutex); + for (const auto& prefix : aliases.blocked) { + if (relative == prefix || (relative.size() > prefix.size() && relative.compare(0, prefix.size(), prefix) == 0 && relative[prefix.size()] == '/')) + throw std::filesystem::filesystem_error("Guest mount is unavailable", guestPath, std::make_error_code(std::errc::no_such_file_or_directory)); + } for (const auto& [prefix, host] : aliases.entries) { if (relative.size() < prefix.size() || relative.compare(0, prefix.size(), prefix) != 0) continue; if (relative.size() == prefix.size()) return std::filesystem::path(host).make_preferred(); @@ -136,6 +141,7 @@ extern "C" void AddPathAlias_nid_no_patch(const char* guestPrefix, const char* h auto& aliases = Aliases(); std::lock_guard lock(aliases.mutex); const auto prefix = TrimSlashes(guestPrefix); + std::erase(aliases.blocked, prefix); for (auto& entry : aliases.entries) { if (entry.first == prefix) { entry.second = hostPath; @@ -211,6 +217,17 @@ extern "C" void SyncWrittenPaths_nid_no_patch() { #endif } +extern "C" void BlockPathAlias_nid_no_patch(const char* guestPrefix) { + if (guestPrefix == nullptr) { APS5_INVALID_ARG_EX; } + const auto prefix = TrimSlashes(guestPrefix); + if (prefix.empty()) { APS5_INVALID_ARG_EX; } + auto& aliases = Aliases(); + std::lock_guard lock(aliases.mutex); + std::erase_if(aliases.entries, [&](const auto& entry) { return entry.first == prefix; }); + std::erase(aliases.blocked, prefix); + aliases.blocked.push_back(prefix); +} + extern "C" std::filesystem::path ResolvePath_nid_no_patch(const char* path) { if (!path) { APS5_INVALID_ARG_EX; } auto& state = Directories(); diff --git a/core/libs/tests/GuestAppContent.cpp b/core/libs/tests/GuestAppContent.cpp index 997e584fa..6780ff814 100644 --- a/core/libs/tests/GuestAppContent.cpp +++ b/core/libs/tests/GuestAppContent.cpp @@ -1,12 +1,21 @@ #include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" +#include #include #include #include #include extern "C" { +int APS5_VABI sceAppContentInitialize(const AppContentInitParam*, AppContentBootParam*); int APS5_VABI sceAppContentAddcontMount(uint32_t, const NpUnifiedEntitlementLabel*, AppContentMountPoint*); int APS5_VABI sceAppContentAddcontUnmount(const AppContentMountPoint*); +int APS5_VABI sceAppContentTemporaryDataMount2(uint32_t, AppContentMountPoint*); +int APS5_VABI sceAppContentTemporaryDataUnmount(const AppContentMountPoint*); +int APS5_VABI sceAppContentTemporaryDataFormat(const AppContentMountPoint*); +int APS5_VABI sceAppContentTemporaryDataGetAvailableSpaceKb(const AppContentMountPoint*, size_t*); +int APS5_VABI access_nid_postfix(const char*, int); +int* APS5_VABI __error_nid_postfix(); int APS5_VABI sceAppContentGetAddcontInfo(uint32_t, const NpUnifiedEntitlementLabel*, void*); int APS5_VABI sceAppContentGetAddcontInfoList(uint32_t, void*, uint32_t, uint32_t*); int APS5_VABI sceAppContentDownloadDataGetAvailableSpaceKb(const AppContentMountPoint*, size_t*); @@ -16,9 +25,15 @@ int APS5_VABI sceAppContentAppParamGetInt(uint32_t, int32_t*); static constexpr int ErrorParameter = static_cast(0x80D90002); static constexpr int ErrorNotFound = static_cast(0x80D90005); static constexpr int ErrorDrmNoEntitlement = static_cast(0x80D90007); +static constexpr int ErrorBusy = static_cast(0x80D90003); +static constexpr int ErrorNotMounted = static_cast(0x80D90004); static void Require(bool value) { if (!value) std::abort(); } int main() { + const auto original = std::filesystem::current_path(); + const auto directory = original / ("appcontent-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(directory)); + std::filesystem::current_path(directory); std::filesystem::create_directories("app0/sce_sys"); { std::ofstream param("app0/sce_sys/param.json", std::ios::binary); @@ -57,6 +72,49 @@ int main() { Require(sceAppContentAddcontUnmount(&mountPoint) == ErrorNotFound); Require(sceAppContentAddcontUnmount(nullptr) == ErrorParameter); + std::filesystem::create_directory(directory / "temp0"); + { std::ofstream file(directory / "temp0" / "old.txt"); file << "previous run"; } + AppContentBootParam boot{}; + Require(sceAppContentInitialize(nullptr, &boot) == 0); + Require(access_nid_postfix("/temp0/old.txt", 0) == -1); + Require(sceAppContentTemporaryDataUnmount(nullptr) == ErrorParameter); + Require(sceAppContentTemporaryDataUnmount(&mountPoint) == ErrorNotMounted); + Require(sceAppContentTemporaryDataMount2(0, nullptr) == ErrorParameter); + std::memset(&mountPoint, 0x5a, sizeof(mountPoint)); + Require(sceAppContentTemporaryDataMount2(2, &mountPoint) == ErrorParameter); + Require(std::memcmp(&mountPoint, &untouched, sizeof(mountPoint)) == 0); + Require(sceAppContentTemporaryDataMount2(0, &mountPoint) == 0); + Require(std::strcmp(mountPoint.data, "/temp0") == 0); + const auto backing = ResolvePath_nid_no_patch("/temp0"); + { std::ofstream file(backing / "keep.txt"); file << "retained"; } + { std::ofstream file(directory / "temp01"); file << "unrelated"; } + AppContentMountPoint second = untouched; + Require(sceAppContentTemporaryDataMount2(1, &second) == ErrorBusy); + Require(std::memcmp(&second, &untouched, sizeof(second)) == 0); + Require(std::filesystem::exists(backing / "keep.txt")); + Require(sceAppContentTemporaryDataUnmount(&second) == ErrorNotMounted); + Require(access_nid_postfix("/temp0/keep.txt", 0) == 0); + size_t available = 0; + Require(sceAppContentTemporaryDataGetAvailableSpaceKb(&mountPoint, &available) == 0 && available > 0); + Require(sceAppContentTemporaryDataUnmount(&mountPoint) == 0); + Require(std::filesystem::exists(backing / "keep.txt")); + Require(access_nid_postfix("/temp0/keep.txt", 0) == -1 && *__error_nid_postfix() == 2); + Require(access_nid_postfix("/temp0", 0) == -1 && *__error_nid_postfix() == 2); + Require(access_nid_postfix("/temp01", 0) == 0); + Require(sceAppContentTemporaryDataUnmount(&mountPoint) == ErrorNotMounted); + available = 123; + Require(sceAppContentTemporaryDataGetAvailableSpaceKb(&mountPoint, &available) == ErrorNotMounted && available == 123); + Require(sceAppContentTemporaryDataFormat(&mountPoint) == ErrorNotMounted); + Require(sceAppContentTemporaryDataMount2(0, &mountPoint) == 0); + Require(access_nid_postfix("/temp0/keep.txt", 0) == 0); + Require(sceAppContentTemporaryDataFormat(nullptr) == ErrorParameter); + Require(sceAppContentTemporaryDataFormat(&mountPoint) == 0); + Require(std::filesystem::is_empty(backing)); + { std::ofstream file(backing / "remove.txt"); file << "format"; } + Require(sceAppContentTemporaryDataUnmount(&mountPoint) == 0); + Require(sceAppContentTemporaryDataMount2(1, &mountPoint) == 0); + Require(std::filesystem::is_empty(backing)); + Require(sceAppContentTemporaryDataUnmount(&mountPoint) == 0); unsigned char info[24]; std::memset(info, 0x5a, sizeof(info)); unsigned char untouchedInfo[24]; @@ -83,4 +141,6 @@ int main() { Require(sceAppContentGetAddcontInfoList(0, nullptr, 0, nullptr) == ErrorParameter); Require(sceAppContentGetAddcontInfoList(0, info, 0, nullptr) == ErrorParameter); Require(sceAppContentGetAddcontInfoList(0, nullptr, 1, nullptr) == ErrorParameter); + std::filesystem::current_path(original); + std::filesystem::remove_all(directory); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 626b7958a..59066aca4 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -318,3 +318,4 @@ Throughout the project, every function at every stage either **does exactly what - [sceImeKeyboardGetResourceId](../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) reports that no USB keyboard is connected (`0x80bc0004`, as [shadPS4](https://github.com/shadps4-emu/shadPS4) does) because IME keyboard events are not delivered to the handler, although [libSceKeyboard](../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp) exposes the host keyboard - [Player review dialog](../../core/libs/prx/libSceCdlgPlayerReview/Export.cpp) (libSceCdlgPlayerReview) shows nothing: `scePlayerReviewDialogOpen` finishes at once and the result reports that the user canceled, as `sceWebBrowserDialogOpen` does - [NP reachability callbacks](../../core/libs/prx/libSceNpManager/Export.cpp) retain one callback and its user data; duplicate registration and unregistering an absent callback return the errors used by [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/libraries/np/np_manager.cpp). No callback is delivered because the existing NP model remains signed out with reachability unavailable. +- [Temporary app-content storage](../../core/libs/prx/libSceAppContent/Export.cpp) tracks one `/temp0` mount, preserves its files on unmount, and rejects new path resolutions until remount. Mount, format, space-query and unmount errors follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_path.pas); the pointer ABI is also declared by [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/AppContent/AppContent.cs). Existing open host descriptors remain usable after unmount; console behaviour with open files is unverified. Host filesystem failures still throw.