feat(libSceAppContent): answer bundle license, playable status and game trials queries

sceAppContentCheckBundleLicenseOnDisc and sceAppContentGetPlayableStatus
answer NOT_FOUND for any label, as dlc_emu does for content it does not
know, after the initialization guard, the null checks and the padded
label check from SDK 0x01500000 or 0x03500000. sceAppContentGetGameTrialsFlag
writes 0, as dlc_emu does. The prototypes are dlc_emu's.
This commit is contained in:
zaryabzip committed 2026-10-31 04:13:11 +05:00
1 parent e9a33a703b
commit fa458d6467
6 files changed
+141 -12

No files matched your search

@@ -13,3 +13,15 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceAppContent>")
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:$<TARGET_FILE_DIR:libSceAppContent>")
endif()
endif()
+17 -6
View File
@@ -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;
}
@@ -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);
@@ -4,9 +4,6 @@
extern "C" {
int APS5_VABI sceAppContentCheckBundleLicenseOnDisc();
int APS5_VABI sceAppContentDownloadDataGetBlockSize();
int APS5_VABI sceAppContentGetGameTrialsFlag();
int APS5_VABI sceAppContentGetPlayableStatus();
}
+108
View File
@@ -0,0 +1,108 @@
#include "prx/libSceAppContent/include/Api.hpp"
#include "SceTypes.hpp"
#include "ProcessParametersFixture.hpp"
#include <array>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <initializer_list>
#include <string>
#ifdef _WIN32
#include <windows.h>
#else
#include <dlfcn.h>
#endif
namespace {
constexpr int NotInitialized = static_cast<int>(0x80D90001u);
constexpr int Parameter = static_cast<int>(0x80D90002u);
constexpr int NotFound = static_cast<int>(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 <typename Function>
Function Resolve(const char* name) {
#ifdef _WIN32
void* symbol = reinterpret_cast<void*>(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<Function>(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<std::byte, 64> 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<CheckBundleLicenseFn>("sceAppContentCheckBundleLicenseOnDisc");
const auto getGameTrialsFlag = Resolve<GetGameTrialsFlagFn>("sceAppContentGetGameTrialsFlag");
const auto getPlayableStatus = Resolve<GetPlayableStatusFn>("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<bool>(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;
}
+1 -3
View File
@@ -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).