diff --git a/core/libs/prx/libSceAppContent/CMakeLists.txt b/core/libs/prx/libSceAppContent/CMakeLists.txt index b1deabb076..669824c193 100644 --- a/core/libs/prx/libSceAppContent/CMakeLists.txt +++ b/core/libs/prx/libSceAppContent/CMakeLists.txt @@ -13,3 +13,15 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING) + add_test_executable(guest_appcontent_license_tests ${LIBS_INCLUDE_DIR}/tests/GuestAppContentLicense.cpp) + target_include_directories(guest_appcontent_license_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_appcontent_license_tests PRIVATE libSceAppContent libc ${CMAKE_DL_LIBS}) + configure_windows_unwind(guest_appcontent_license_tests) + add_test(NAME guest_appcontent_license COMMAND guest_appcontent_license_tests) + if(WIN32) + set_tests_properties(guest_appcontent_license PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceAppContent/Export.cpp b/core/libs/prx/libSceAppContent/Export.cpp index 478f72de02..4015a2aea5 100644 --- a/core/libs/prx/libSceAppContent/Export.cpp +++ b/core/libs/prx/libSceAppContent/Export.cpp @@ -70,20 +70,31 @@ static void ClearDirectory(const std::filesystem::path& directory) { extern "C" { -int APS5_VABI sceAppContentCheckBundleLicenseOnDisc() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceAppContentCheckBundleLicenseOnDisc(uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; + (void)serviceLabel; + if (!entitlementLabel) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (!validEntitlementLabel(*entitlementLabel, 0x01500000)) return SCE_APP_CONTENT_ERROR_PARAMETER; + return SCE_APP_CONTENT_ERROR_NOT_FOUND; } int APS5_VABI sceAppContentDownloadDataGetBlockSize() { NotImplemented_nid_no_patch(__func__); } -int APS5_VABI sceAppContentGetGameTrialsFlag() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceAppContentGetGameTrialsFlag(uint32_t* gameTrialsFlag) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; + if (!gameTrialsFlag) return SCE_APP_CONTENT_ERROR_PARAMETER; + *gameTrialsFlag = 0; + return 0; } -int APS5_VABI sceAppContentGetPlayableStatus() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceAppContentGetPlayableStatus(uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel, uint32_t* playableStatus) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; + (void)serviceLabel; + if (!entitlementLabel || !playableStatus) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (!validEntitlementLabel(*entitlementLabel, 0x03500000)) return SCE_APP_CONTENT_ERROR_PARAMETER; + return SCE_APP_CONTENT_ERROR_NOT_FOUND; } diff --git a/core/libs/prx/libSceAppContent/include/Api.hpp b/core/libs/prx/libSceAppContent/include/Api.hpp index a48561a66e..ae24effec5 100644 --- a/core/libs/prx/libSceAppContent/include/Api.hpp +++ b/core/libs/prx/libSceAppContent/include/Api.hpp @@ -32,6 +32,7 @@ int APS5_VABI sceAppContentAddcontEnqueueDownloadByEntitlemetId(const char* enti int APS5_VABI sceAppContentAddcontEnqueueDownloadSp(uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel); int APS5_VABI sceAppContentAddcontMountByEntitlemetId(const char* entitlementId, AppContentMountPoint* mountPoint); int APS5_VABI sceAppContentAddcontShrink(uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel); +int APS5_VABI sceAppContentCheckBundleLicenseOnDisc(uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel); int APS5_VABI sceAppContentAppParamGetString(void); int APS5_VABI sceAppContentDownload0Expand(void); int APS5_VABI sceAppContentDownload0Shrink(void); @@ -41,7 +42,9 @@ int APS5_VABI sceAppContentGetAddcontInfoByEntitlementId(const char* entitlement int APS5_VABI sceAppContentGetAddcontInfoListByIroTag(uint32_t iroTag, AppContentAddcontInfo* list, uint32_t listNum, uint32_t* hitNum); int APS5_VABI sceAppContentGetDownloadedStoreCountry(void); int APS5_VABI sceAppContentGetEntitlementKey(uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel, AppContentEntitlementKey* key); +int APS5_VABI sceAppContentGetGameTrialsFlag(uint32_t* gameTrialsFlag); int APS5_VABI sceAppContentGetPftFlag(uint32_t* pftFlag); +int APS5_VABI sceAppContentGetPlayableStatus(uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel, uint32_t* playableStatus); int APS5_VABI sceAppContentGetRegion(void); int APS5_VABI sceAppContentRequestPatchInstall(void); int APS5_VABI sceAppContentTemporaryDataMount(AppContentMountPoint* mountPoint); diff --git a/core/libs/prx/libSceAppContent/include/InventoryExports.hpp b/core/libs/prx/libSceAppContent/include/InventoryExports.hpp index 2c7f64dfb8..fba072b0cb 100644 --- a/core/libs/prx/libSceAppContent/include/InventoryExports.hpp +++ b/core/libs/prx/libSceAppContent/include/InventoryExports.hpp @@ -4,9 +4,6 @@ extern "C" { -int APS5_VABI sceAppContentCheckBundleLicenseOnDisc(); int APS5_VABI sceAppContentDownloadDataGetBlockSize(); -int APS5_VABI sceAppContentGetGameTrialsFlag(); -int APS5_VABI sceAppContentGetPlayableStatus(); } diff --git a/core/libs/tests/GuestAppContentLicense.cpp b/core/libs/tests/GuestAppContentLicense.cpp new file mode 100644 index 0000000000..8a19ce0a71 --- /dev/null +++ b/core/libs/tests/GuestAppContentLicense.cpp @@ -0,0 +1,108 @@ +#include "prx/libSceAppContent/include/Api.hpp" +#include "SceTypes.hpp" +#include "ProcessParametersFixture.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#endif + +namespace { + +constexpr int NotInitialized = static_cast(0x80D90001u); +constexpr int Parameter = static_cast(0x80D90002u); +constexpr int NotFound = static_cast(0x80D90005u); +constexpr std::uint32_t Untouched = 0x5a5a5a5au; + +using CheckBundleLicenseFn = int (APS5_VABI*)(std::uint32_t, const NpUnifiedEntitlementLabel*); +using GetGameTrialsFlagFn = int (APS5_VABI*)(std::uint32_t*); +using GetPlayableStatusFn = int (APS5_VABI*)(std::uint32_t, const NpUnifiedEntitlementLabel*, std::uint32_t*); + +template +Function Resolve(const char* name) { +#ifdef _WIN32 + void* symbol = reinterpret_cast(GetProcAddress(LoadLibraryA("libSceAppContent.prx"), name)); +#else + void* symbol = dlsym(RTLD_DEFAULT, name); +#endif + if (!symbol) { + std::fprintf(stderr, "%s is not exported by libSceAppContent\n", name); + std::abort(); + } + return reinterpret_cast(symbol); +} + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "AppContent license: %s\n", message); + std::abort(); +} + +} + +int main() { + static std::array process{}; + Require(ProcessParametersFixture::Install(process.data(), process.size()), "the process parameters could not be installed"); + std::uint32_t sdkVersion = 0x03500000; + std::memcpy(process.data() + 16, &sdkVersion, sizeof(sdkVersion)); + + const auto checkBundleLicense = Resolve("sceAppContentCheckBundleLicenseOnDisc"); + const auto getGameTrialsFlag = Resolve("sceAppContentGetGameTrialsFlag"); + const auto getPlayableStatus = Resolve("sceAppContentGetPlayableStatus"); + + NpUnifiedEntitlementLabel label{}; + std::memcpy(label.data, "TESTADDON0000001", 17); + std::uint32_t flag = Untouched; + std::uint32_t status = Untouched; + Require(checkBundleLicense(0, &label) == NotInitialized, "the bundle license was checked before initialization"); + Require(getGameTrialsFlag(&flag) == NotInitialized && flag == Untouched, "the game trials flag was read before initialization"); + Require(getPlayableStatus(0, &label, &status) == NotInitialized && status == Untouched, "the playable status was read before initialization"); + + const auto original = std::filesystem::current_path(); + const auto directory = original / ("appcontent-license-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(directory), "the test directory could not be created"); + std::filesystem::current_path(directory); + std::filesystem::create_directories("app0/sce_sys"); + { + std::ofstream param("app0/sce_sys/param.json", std::ios::binary); + param << R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":"Example"}},"downloadDataSize":0})"; + Require(static_cast(param), "param.json could not be written"); + } + AppContentInitParam init{}; + AppContentBootParam boot{}; + Require(sceAppContentInitialize(&init, &boot) == 0, "the library could not be initialized"); + + Require(checkBundleLicense(0, &label) == NotFound && checkBundleLicense(7, &label) == NotFound, "a bundle license was found for unknown content"); + Require(checkBundleLicense(0, nullptr) == Parameter, "a null label was taken by the bundle license check"); + Require(getGameTrialsFlag(&flag) == 0 && flag == 0, "the game trials flag was not 0"); + Require(getGameTrialsFlag(nullptr) == Parameter, "a null game trials flag was taken"); + Require(getPlayableStatus(0, &label, &status) == NotFound && getPlayableStatus(7, &label, &status) == NotFound && status == Untouched, "a playable status was given for unknown content"); + Require(getPlayableStatus(0, nullptr, &status) == Parameter && getPlayableStatus(0, &label, nullptr) == Parameter && status == Untouched, "a null argument was taken by the playable status"); + + for (const std::uint32_t version : {0x014fffffu, 0x01500000u, 0x034fffffu, 0x03500000u}) { + std::memcpy(process.data() + 16, &version, sizeof(version)); + for (char& padding : label.padding) { + padding = 1; + Require(checkBundleLicense(0, &label) == (version >= 0x01500000 ? Parameter : NotFound), "a padded label was not checked from SDK 0x01500000 by the bundle license check"); + Require(getPlayableStatus(0, &label, &status) == (version >= 0x03500000 ? Parameter : NotFound) && status == Untouched, "a padded label was not checked from SDK 0x03500000 by the playable status"); + padding = 0; + } + } + + std::filesystem::current_path(original); + std::filesystem::remove_all(directory); + std::puts("AppContent license and trials tests passed"); + return 0; +} diff --git a/docs/dev/debt/libSceAppContent.md b/docs/dev/debt/libSceAppContent.md index c75f621a36..b2b47c305d 100644 --- a/docs/dev/debt/libSceAppContent.md +++ b/docs/dev/debt/libSceAppContent.md @@ -6,10 +6,7 @@ | Name | NID | | --- | --- | -| `sceAppContentCheckBundleLicenseOnDisc` | `xZo2-418Wdo` | | `sceAppContentDownloadDataGetBlockSize` | `9ht66cHTj7g` | -| `sceAppContentGetGameTrialsFlag` | `1saJukIkcKw` | -| `sceAppContentGetPlayableStatus` | `y8meQn-Qy5c` | - [sceAppContentDownloadDataGetAvailableSpaceKb](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - a title whose `param.json` declares `downloadDataSize` 0 gets 0 KB and OK; the console's behaviour for a title without a download data area is unverified (it may return an error instead). KytyPS5 does not special-case it, and shadPS4 stubs 1048576 KB. One disassembled title (PPSA15246) calls it with the `/download0` mount point and a zeroed 64-bit output, ignores the return value and multiplies the output by 1024, so for that title an error that leaves the output untouched and an OK with 0 KB look the same - [sceAppContentDownloadDataFormat](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - empties the `/download0` directory, creating it if needed. Imported by PPSA01317. A null mount point returns `PARAMETER` and another mount point returns `NOT_MOUNTED` as in `sceAppContentTemporaryDataFormat` and `sceAppContentDownloadDataGetAvailableSpaceKb`; the console's error codes for both and its behaviour with files still open are unverified @@ -23,5 +20,6 @@ - [Additional content](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries; `sceAppContentAddcontEnqueueDownload` answers `PARAMETER` for a null label and `NOT_FOUND` otherwise, since no download is ever queued; `sceAppContentAddcontDelete` answers `PARAMETER` for a null label and `NOT_FOUND` otherwise, since no additional content is ever installed. `sceAppContentGetAddcontDownloadProgress` answers `PARAMETER` for a null label or progress and `NOT_FOUND` otherwise, as [dlc_emu](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.cpp#L2328-L2341) does for a label that is not installed additional content, and never writes the progress. Its 16-byte layout (`DataSize`, then `DownloadedSize`, both 64-bit) follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/AppContent/AppContent.cs#L45-L54) and is unused here. The signatures of `sceAppContentAddcontEnqueueDownload` and `sceAppContentGetAddcontDownloadProgress` follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/AppContent/AppContent.cs#L140-L148), and the signature of `sceAppContentAddcontDelete` follows [dlc_emu](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.h#L91-L92); dlc_emu is not a console measurement and no console source gives the error codes, so the signatures and the error codes are unverified - [sceAppContentAddcontEnqueueDownloadByEntitlemetId, sceAppContentAddcontEnqueueDownloadSp, sceAppContentAddcontMountByEntitlemetId and sceAppContentAddcontShrink](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - unverified on a PS5. After the initialization guard they answer `PARAMETER` for a null argument and `NOT_FOUND` otherwise, without writing the mount point, as [dlc_emu](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.cpp#L2125-L2209) does for content it does not know; the prototypes are [dlc_emu's](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.h#L88-L99). dlc_emu rejects a padded label at every SDK while the label calls above check it only from SDK 0x01500000 or 0x03500000, so `Sp` and `Shrink` answer `PARAMETER` for one from SDK 0x03500000 and throw below it - [sceAppContentGetAddcontInfoByEntitlementId, sceAppContentGetAddcontInfoListByIroTag and sceAppContentGetPftFlag](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - unverified on a PS5. After the initialization guard they give [dlc_emu's](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.cpp#L2040-L2086) answers for content it does not know: `DRM_NO_ENTITLEMENT` without writing the info, 0 entries for any tag (a null `hitNum` beside a list returns 0, unlike `sceAppContentGetAddcontInfoList`), and a [PFT flag of 0](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.cpp#L2343-L2347), and `PARAMETER` for null arguments, with dlc_emu's [prototypes](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.h#L74-L131) +- [sceAppContentCheckBundleLicenseOnDisc, sceAppContentGetPlayableStatus and sceAppContentGetGameTrialsFlag](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - unverified on a PS5. After the initialization guard they give [dlc_emu's](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.cpp#L2211-L2256) answers for content it does not know: `NOT_FOUND` for both label calls without writing the playable status, and a game trials flag of 0. Null arguments return `PARAMETER`, and so does a padded label from SDK 0x01500000 (CheckBundleLicenseOnDisc) or 0x03500000 (GetPlayableStatus), the versions [dlc_emu checks](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.cpp#L1570-L1576) them from. The prototypes are [dlc_emu's](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.h#L100-L109) - [sceAppContentGetEntitlementKey](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - the service result without an entitlement is unverified: this model returns `DRM_NO_ENTITLEMENT` and leaves the 16-byte key unchanged. Initialization, null-pointer and SDK 0x01500000 label-padding checks match the reported [firmware 13.42 module probe](https://github.com/boykopovar/AnyPS5/pull/9638#issuecomment-6390573735), run in an emulator, not on hardware. That module forwards service labels 0, 7, 8 and 0xFFFFFFFF without checking them; this library ignores the service label. The system service's own validation and errors remain unknown. - [Temporary app-content storage](../../../core/libs/prx/libSceAppContent/Export.cpp) console behaviour and hardware return values are unverified, including behaviour with open files across unmount; host filesystem failures throw. Storage models one `/temp0` mount, preserving files on unmount while rejecting new path resolutions until remount, and leaving existing open host descriptors usable. `sceAppContentTemporaryDataMount` delegates to `sceAppContentTemporaryDataMount2` with option 1 (`TEMPORARY_DATA_OPTION_FORMAT`) to format on mount as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/app_content/app_content.cpp#L550-L553) does. Mount, format, space-query and unmount errors follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_path.pas); pointer ABI follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/AppContent/AppContent.cs).