#include "prx/libSceNpCommon/include/Api.hpp" #include "SceTypes.hpp" #include #include #include #include #include #include #include #include namespace { constexpr int NpIpcAlreadyInitialized = static_cast(0x80559D00); constexpr int NpIpcNotInitialized = static_cast(0x80559D01); constexpr int NpOutOfMemory = static_cast(0x80550005); constexpr int NpNullObject = static_cast(0x80558005); constexpr int NpNotInitialized = static_cast(0x80558001); constexpr int NpAlreadyInitialized = static_cast(0x80558002); constexpr int NpAlreadyQueued = static_cast(0x80558006); constexpr int NpUtilInvalid = static_cast(0x80550601); constexpr int NpUtilBufferTooSmall = static_cast(0x80550602); constexpr int NpUtilOutOfRange = static_cast(0x8055060A); constexpr int NpUtilInvalidCharacter = static_cast(0x80550608); constexpr int NpInvalidFormat = static_cast(0x80558003); constexpr int NpInvalidCharacter = static_cast(0x80558004); constexpr int NpBufferTooSmall = static_cast(0x80558008); constexpr int NpOutOfRange = static_cast(0x8055800E); constexpr int NpInvalidArgument = static_cast(0x80558009); constexpr int NpEventFlagBusy = static_cast(0x8055800C); constexpr int NpSemaBusy = static_cast(0x8055800D); constexpr int KernelNotPermitted = static_cast(0x80020001); constexpr int KernelNoSuchObject = static_cast(0x80020003); constexpr std::uint32_t EventFlagWaitAnd = 1; constexpr std::uint32_t EventFlagShared = 0x100; void Require(bool value, const char* message) { if (value) return; std::fprintf(stderr, "%s\n", message); std::abort(); } void* APS5_VABI ReturnArgument(void* arg) { Pthread self {}; Require(sceNpThreadGetId(&self) == 0 && self != nullptr, "a created thread can read its id"); return arg; } void TestAtomics() { std::int32_t value = 5; Require(sceNpAtomicInc32(&value) == 5 && value == 6, "increment returns the previous value"); Require(sceNpAtomicDec32(&value) == 6 && value == 5, "decrement returns the previous value"); Require(sceNpAtomicCas32(&value, 4, 9) == 5 && value == 5, "a failed compare-and-swap returns the current value"); Require(sceNpAtomicCas32(&value, 5, 9) == 5 && value == 9, "a successful compare-and-swap returns the previous value"); } void TestSemaphoresAndFlags() { KernelSema sema {}; Require(sceNpCreateSema(&sema, 0, 0, 2, "NpSema") == 0, "a semaphore is created"); Require(sceNpPollSema(sema) == NpSemaBusy, "an empty semaphore reports the busy error"); Require(sceNpSignalSema(sema) == 0 && sceNpPollSema(sema) == 0, "a signalled semaphore can be taken once"); Require(sceNpDeleteSema(sema) == 0, "the semaphore is deleted"); KernelEventFlag flag {}; Require(sceNpCreateEventFlag(&flag, "NpFlag", 0, 0) == 0, "an event flag is created"); std::uint64_t result = 0; Require(sceNpPollEventFlag(flag, 1, EventFlagWaitAnd, &result) == NpEventFlagBusy, "a clear event flag reports the busy error"); Require(sceNpSetEventFlag(flag, 1) == 0 && sceNpPollEventFlag(flag, 1, EventFlagWaitAnd, &result) == 0 && result == 1, "a set bit is polled"); Require(sceNpDeleteEventFlag(flag) == 0, "the event flag is deleted"); } void TestSharedEventFlags() { KernelEventFlag creator {}; Require(sceNpCreateEventFlag(&creator, "NpSharedFlag", EventFlagShared, 0) == 0, "a shared event flag is created"); KernelEventFlag opened {}; Require(sceNpOpenEventFlag(&opened, "NpSharedFlag") == 0 && opened != creator, "the shared event flag is opened by name"); std::uint64_t result = 0; Require(sceNpSetEventFlag(opened, 2) == 0 && sceNpPollEventFlag(creator, 2, EventFlagWaitAnd, &result) == 0 && result == 2, "a bit set through the opened handle is polled through the creator"); Require(sceNpCloseEventFlag(creator) == KernelNotPermitted, "the creator handle is not closed"); Require(sceNpCloseEventFlag(opened) == 0, "the opened handle is closed"); Require(sceNpCloseEventFlag(opened) == KernelNoSuchObject, "a closed handle is gone"); Require(sceNpDeleteEventFlag(creator) == 0, "the creator deletes the shared event flag"); } void TestThreads() { Require(sceNpThreadGetId(nullptr) == NpInvalidArgument, "a null thread id output is refused"); Pthread thread {}; int marker = 0; Require(sceNpCreateThread(&thread, ReturnArgument, &marker, 0, 0x10000, 0, "NpThread") == 0, "a thread is created"); void* value = nullptr; Require(sceNpJoinThread(thread, &value) == 0 && value == &marker, "the thread is joined with its return value"); } void UseAllocator(const NpAllocator& allocator) { auto* block = static_cast(allocator.allocate(64, allocator.context)); Require(block != nullptr, "the allocator hands out memory"); block[63] = 0x5A; block = static_cast(allocator.reallocate(block, 256, allocator.context)); Require(block != nullptr && block[63] == 0x5A, "reallocation keeps the contents"); allocator.release(block, allocator.context); allocator.release(nullptr, allocator.context); } void TestHeaps() { NpMemoryHeap memoryHeap {}; Require(sceNpMemoryHeapInit(nullptr, 0x1000, "np-test") == NpNullObject, "a null memory heap is refused"); Require(sceNpMemoryHeapInit(&memoryHeap, 0x1001, "np-test") == 0, "a memory heap is created"); Require(memoryHeap.memory.size == 0x4000 && memoryHeap.memory.address != nullptr && memoryHeap.heap.mspace != nullptr, "the memory heap size is rounded up to 16 KiB"); NpAllocatorEx allocatorEx {}; sceNpMemoryHeapGetAllocatorEx(&memoryHeap, 0x1234, &allocatorEx); Require(allocatorEx.allocator.context == &memoryHeap && allocatorEx.outOfMemoryError == 0x1234, "the extended allocator carries the caller's error code"); UseAllocator(allocatorEx.allocator); sceNpMemoryHeapDestroy(&memoryHeap); Require(memoryHeap.memory.address == nullptr && memoryHeap.heap.mspace == nullptr, "destroying clears the memory heap"); NpKernelMemory memory {}; void* address = nullptr; Require(sceNpAllocateKernelMemoryWithAlignment(0x10, "np-test", &address, nullptr) == NpNullObject, "kernel memory needs an object"); Require(sceNpAllocateKernelMemoryWithAlignment(0x10, "np-test", &address, &memory) == 0 && address == memory.address && memory.size == 0x4000, "kernel memory is mapped in 16 KiB units"); NpHeap heap {}; Require(sceNpHeapInit(&heap, memory.address, memory.size, "np-test") == 0 && heap.mspace != nullptr, "a heap is created"); NpAllocator allocator {}; sceNpHeapGetAllocator(&heap, &allocator); Require(allocator.context == &heap, "the heap allocator uses the heap as its context"); UseAllocator(allocator); NpHeapStat stat {}; Require(sceNpHeapGetStat(&heap, &stat) == 0 && stat.maxInuseSize >= stat.currentInuseSize, "heap statistics are reported"); sceNpHeapDestroy(&heap); Require(heap.mspace == nullptr, "destroying clears the heap"); Require(sceNpFreeKernelMemory(&memory) == 0 && memory.address == nullptr, "kernel memory is released"); Require(sceNpFreeKernelMemory(nullptr) == NpNullObject, "freeing needs an object"); Require(sceNpArchInit() == 0 && sceNpArchInit() == 0, "the library initialises more than once"); NpAllocatorEx* global = sceNpGlobalHeapGetAllocatorExPtr(); Require(global->outOfMemoryError == NpOutOfMemory, "the global allocator reports the library's out-of-memory error"); UseAllocator(global->allocator); sceNpArchTerm(); UseAllocator(sceNpGlobalHeapGetAllocatorExPtr()->allocator); sceNpArchTerm(); Require(sceNpIpc2ClientTerm() == NpIpcNotInitialized, "the ipc client must be started before it stops"); Require(sceNpIpc2ClientInit() == 0 && sceNpIpc2ClientInit() == NpIpcAlreadyInitialized, "the ipc client starts once"); Require(sceNpIpc2ClientTerm() == 0 && sceNpIpc2ClientInit() == 0 && sceNpIpc2ClientTerm() == 0, "the ipc client restarts"); } struct CalloutRecord { std::atomic order {0}; std::atomic first {0}; std::atomic second {0}; }; CalloutRecord g_callouts; void APS5_VABI RecordFirst(void*) { g_callouts.first = ++g_callouts.order; } void APS5_VABI RecordSecond(void*) { g_callouts.second = ++g_callouts.order; } void APS5_VABI RecordUnexpected(void*) { Require(false, "a stopped callout does not run"); } void TestCallouts() { NpCalloutContext context {}; NpCallout callout {}; Require(sceNpCalloutStartOnCtx(&context, &callout, 0, RecordFirst, nullptr) == NpNotInitialized, "a context must be initialised first"); Require(sceNpCalloutInitCtx(&context, "np-callout", 0x4000, 0, 0) == 0, "a callout context starts"); Require(sceNpCalloutInitCtx(&context, "np-callout", 0x4000, 0, 0) == NpAlreadyInitialized, "a context starts once"); NpCallout stopped {}; int wasPending = 0; Require(sceNpCalloutStartOnCtx(&context, &stopped, 60000000, RecordUnexpected, nullptr) == 0, "a distant callout is queued"); Require(sceNpCalloutStartOnCtx(&context, &stopped, 60000000, RecordUnexpected, nullptr) == NpAlreadyQueued, "a queued callout cannot be queued again"); Require(sceNpCalloutStopOnCtx(&context, &stopped, &wasPending) == 0 && wasPending == 1, "a queued callout is removed"); Require(sceNpCalloutStopOnCtx(&context, &stopped, &wasPending) == 0 && wasPending == 0, "removing twice reports nothing pending"); NpCallout queuedFirst {}; NpCallout queuedSecond {}; Require(sceNpCalloutStartOnCtx(&context, &queuedFirst, 30000000, RecordUnexpected, nullptr) == 0, "an earlier distant callout is queued"); Require(sceNpCalloutStartOnCtx(&context, &queuedSecond, 60000000, RecordUnexpected, nullptr) == 0, "a later distant callout is queued"); const std::uint64_t queuedDeadline = queuedFirst.deadline; Require(sceNpCalloutStartOnCtx(&context, &queuedFirst, 0, RecordFirst, &context) == NpAlreadyQueued, "the earlier callout cannot be queued again"); Require(queuedFirst.next == &queuedSecond && queuedFirst.deadline == queuedDeadline && queuedFirst.handler == RecordUnexpected && queuedFirst.arg == nullptr, "a rejected start leaves the queued callout unchanged"); Require(sceNpCalloutStopOnCtx(&context, &queuedSecond, &wasPending) == 0 && wasPending == 1, "the later callout is still pending after the rejected start"); Require(sceNpCalloutStopOnCtx(&context, &queuedFirst, &wasPending) == 0 && wasPending == 1, "the earlier callout is still pending"); NpCallout later {}; Require(sceNpCalloutStartOnCtx(&context, &later, 20000, RecordSecond, nullptr) == 0, "a later callout is queued"); Require(sceNpCalloutStartOnCtx(&context, &callout, 1000, RecordFirst, nullptr) == 0, "an earlier callout is queued"); for (int i = 0; i < 2000 && g_callouts.second == 0; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1)); Require(g_callouts.first == 1 && g_callouts.second == 2, "callouts run in deadline order"); sceNpCalloutTermCtx(&context); Require(context.initialized == 0, "terminating clears the context"); sceNpCalloutTermCtx(&context); } bool Equals(const char* actual, const char* expected) { return std::strcmp(actual, expected) == 0; } void TestIds() { Require(sceNpUtilCmpAccountId(7, 7) == 0 && sceNpUtilCmpAccountId(7, 8) == 1, "account ids compare for equality"); constexpr int NullArgument = static_cast(0x80550003); constexpr int InvalidNpId = static_cast(0x80550605); constexpr int NotMatch = static_cast(0x80550609); auto makeNpId = [](const char* name, std::uint32_t opt, std::uint32_t platform) { std::array id{}; std::memcpy(id.data(), name, std::strlen(name)); std::memcpy(id.data() + 0x14, &opt, sizeof(opt)); std::memcpy(id.data() + 0x18, &platform, sizeof(platform)); id[0x1C] = 1; return id; }; const auto alice = makeNpId("Alice_01", 5, 0x11223344); auto other = alice; Require(sceNpCmpNpId(nullptr, alice.data()) == NullArgument && sceNpCmpNpId(alice.data(), nullptr) == NullArgument, "null NpIds are refused"); Require(sceNpCmpNpId(alice.data(), other.data()) == 0, "identical NpIds match"); other[0x1C] = 0; Require(sceNpCmpNpId(alice.data(), other.data()) == InvalidNpId && sceNpCmpNpId(other.data(), alice.data()) == InvalidNpId, "the flag byte at 0x1C must be 1"); Require(sceNpCmpNpId(makeNpId("Bob-2", 5, 0x11223344).data(), alice.data()) == NotMatch, "different online ids do not match"); Require(sceNpCmpNpId(makeNpId("Alice_01", 6, 0x11223344).data(), alice.data()) == NotMatch, "different words at 0x14 do not match"); Require(sceNpCmpNpId(makeNpId("Alice_01", 5, 0).data(), alice.data()) == 0, "an empty first byte at 0x18 on the first id matches"); Require(sceNpCmpNpId(alice.data(), makeNpId("Alice_01", 5, 0).data()) == 0, "an empty first byte at 0x18 on the second id matches"); Require(sceNpCmpNpId(alice.data(), makeNpId("Alice_01", 5, 0x11223355).data()) == NotMatch, "different words at 0x18 do not match"); Require(sceNpIntIsOnlineIdString("Alice_01") && sceNpIntIsOnlineIdString("Bob-2"), "letters, digits, '_' and '-' are allowed"); Require(!sceNpIntIsOnlineIdString("ab") && !sceNpIntIsOnlineIdString("abcdefghijklmnopq"), "online ids are 3 to 16 characters"); Require(!sceNpIntIsOnlineIdString("1abc") && !sceNpIntIsOnlineIdString("ab.c") && !sceNpIntIsOnlineIdString(nullptr), "online ids start with a letter and use no other symbols"); } void TestJsonText() { char escaped[64] = {}; Require(sceNpUtilJsonEscape(escaped, sizeof(escaped), "a\"b/\n\x01", 7) == 0 && Equals(escaped, "a\\\"b\\/\\n\\u0001"), "json escaping covers quotes, solidus, newlines and controls"); std::strcpy(escaped, "x"); Require(sceNpUtilJsonEscape(escaped, sizeof(escaped), "yz", 2) == 0 && Equals(escaped, "xyz"), "escaping appends to the buffer"); Require(sceNpUtilJsonEscape(escaped, 6, "a", 1) == NpUtilBufferTooSmall, "escaping needs room for one escape"); Require(sceNpUtilJsonEscape(escaped, sizeof(escaped), nullptr, 1) == NpUtilInvalid, "escaping needs a source"); struct Guarded { char buffer[16]; char canary[16]; } guarded{}; std::strcpy(guarded.buffer, "0123456789"); Require(sceNpUtilJsonEscape(guarded.buffer, sizeof(guarded.buffer), "\n\n\n", 3) == NpUtilBufferTooSmall && guarded.canary[0] == 0, "escaping into a partly filled buffer counts the text already in it"); std::memset(guarded.buffer, 'x', sizeof(guarded.buffer)); Require(sceNpUtilJsonEscape(guarded.buffer, sizeof(guarded.buffer), "a", 1) == NpUtilBufferTooSmall && guarded.canary[0] == 0, "escaping into a buffer without a terminator is refused"); char text[32] = {}; std::size_t length = 0; Require(sceNpUtilJsonUnescape(text, sizeof(text), "a\\nb\\u0041", 10, &length) == 0 && Equals(text, "a\nbA") && length == 5, "json unescaping decodes escapes and counts the terminator"); Require(sceNpUtilJsonUnescape(text, sizeof(text), "\\uD83D\\uDE00", 12, &length) == 0 && Equals(text, "\xF0\x9F\x98\x80") && length == 5, "surrogate pairs become one UTF-8 character"); Require(sceNpUtilJsonUnescape(text, sizeof(text), "\\u00G1", 6, &length) == NpUtilBufferTooSmall && Equals(text, "\\u00G1"), "a bad unicode escape is copied and reported"); Require(sceNpUtilJsonUnescape(nullptr, sizeof(text), "abc", 3, &length) == 0 && length == 4, "a null buffer reports the size"); Require(sceNpUtilJsonUnescape(text, 3, "abc", 3, &length) == NpUtilBufferTooSmall, "unescaping needs room for the terminator"); length = 0; Require(sceNpUtilJsonUnescape(nullptr, 0, "abc", 3, &length) == 0 && length == 4, "a size query with no buffer and no size reports the size"); char one[4] = {}; std::size_t consumed = 0; Require(sceNpUtilJsonGetOneChar(one, sizeof(one), "\\u00e9x", 7, &length, &consumed) == 0 && length == 2 && consumed == 6 && std::memcmp(one, "\xC3\xA9", 2) == 0, "one escaped character is decoded"); Require(sceNpUtilJsonGetOneChar(one, sizeof(one), "\xE6\x97\xA5", 3, &length, &consumed) == 0 && length == 3 && consumed == 3, "one UTF-8 character is copied"); Require(sceNpUtilJsonGetOneChar(one, sizeof(one), "\xE6\x97", 2, &length, &consumed) == NpUtilBufferTooSmall && length == 0 && consumed == 3, "a cut UTF-8 character reports its full length"); Require(sceNpUtilJsonGetOneChar(one, 1, "\xC3\xA9", 2, &length, &consumed) == NpUtilBufferTooSmall && length == 2, "a short buffer still reports the decoded size"); Require(sceNpUtilJsonGetOneChar(one, sizeof(one), "\\ud83dXXde00", 12, &length, &consumed) == NpUtilBufferTooSmall, "a high surrogate without a second unicode escape is not combined"); const char* character = nullptr; const char* japanese = "\xE6\x97\xA5" "a"; Require(sceNpUtilGetNthChar(1, japanese, &character) == 0 && character == japanese + 3, "characters are counted, not bytes"); Require(sceNpUtilGetNthChar(10, "abc", &character) == 0 && *character == '\0', "counting stops at the end"); Require(sceNpUtilGetNthChar(2, "a\x01b", &character) == NpUtilInvalidCharacter, "control characters are refused"); char url[32] = {}; Require(sceNpUtilHttpUrlEncode(url, sizeof(url), "a b=c*", 6, &length) == 0 && Equals(url, "a+b%3Dc*") && length == 9, "form encoding keeps the safe set and escapes the rest"); Require(sceNpUtilHttpUrlEncode(nullptr, 0, "@", 1, &length) == 0 && length == 4, "a null buffer reports the encoded size"); Require(sceNpUtilHttpUrlEncode(url, 3, "@", 1, &length) == NpUtilBufferTooSmall, "encoding needs room for the terminator"); } void TestTitleIds() { NpTitleIdParts parts {{'A', 'B', 'C', 'D', '1', '2', '3', '4', '5'}, 0, 7}; char titleId[13] = {}; Require(sceNpUtilBuildTitleId(&parts, titleId, sizeof(titleId)) == 0 && Equals(titleId, "ABCD12345_07"), "the index is two digits"); Require(sceNpUtilBuildTitleId(&parts, titleId, 12) == NpUtilBufferTooSmall, "a title id needs 13 bytes"); parts.index = 100; Require(sceNpUtilBuildTitleId(&parts, titleId, sizeof(titleId)) == NpUtilOutOfRange, "the index is at most 99"); } void TestLanguagesAndDebugIds() { char code[6] = {}; char tag[36] = {}; Require(sceNpUtilGetNpLanguageCode(1, code) == 0 && Equals(code, "en"), "language 1 is English"); Require(sceNpUtilGetNpLanguageCode2(20, tag) == 0 && Equals(tag, "es-419"), "language 20 is Latin American Spanish"); Require(Equals(sceNpUtilGetNpLanguageCodeStr(11), "zh-CN") && Equals(sceNpUtilGetNpLanguageCode2Str(30), "uk-UA"), "language strings are returned directly"); Require(sceNpUtilGetNpLanguageCode(31, code) == NpUtilInvalid && sceNpUtilGetNpLanguageCodeStr(31) == nullptr, "unknown languages are refused"); Require(sceNpUtilGetNpLanguageCode2(0, nullptr) == NpUtilInvalid, "a language code needs a buffer"); const std::uint32_t first = sceNpDbgAssignDebugId(); Require(first != 0 && sceNpDbgAssignDebugId() == first + 1, "debug ids count up"); } void TestBase64() { const std::uint8_t payload[] = {1, 2, 3, 4, 5}; char text[16] = {}; Require(sceNpBase64Encoder(payload, text, 5) == 8 && Equals(text, "AQIDBAU="), "base64 pads the last group"); Require(sceNpBase64Encoder(payload, text, 0) == 0, "an empty payload encodes to nothing"); Require(sceNpBase64GetDecodeSize("AQIDBAU=", 8) == 5 && sceNpBase64GetDecodeSize("AQ==", 4) == 1, "the decoded size follows the padding"); Require(sceNpBase64GetDecodeSize("", 0) == NpInvalidFormat && sceNpBase64GetDecodeSize("AQI", 3) == NpInvalidFormat, "base64 needs whole groups"); Require(sceNpBase64GetDecodeSize("AQ=A", 4) == NpInvalidCharacter && sceNpBase64GetDecodeSize("A=AAAQID", 8) == NpInvalidCharacter, "padding is only allowed at the end"); std::uint8_t bytes[8] = {}; Require(sceNpBase64Decoder("AQIDBAU=", 8, bytes, sizeof(bytes)) == 5 && std::memcmp(bytes, payload, 5) == 0, "base64 decodes back to the payload"); Require(sceNpBase64Decoder("AQID", 4, bytes, 2) == NpInvalidFormat, "decoding refuses a short buffer"); Require(sceNpBase64Decoder("AQ?D", 4, bytes, 3) == NpInvalidCharacter, "decoding refuses characters outside the alphabet"); const std::uint8_t url[] = {0xFB, 0x00}; std::memset(text, 0, sizeof(text)); Require(sceNpBase64UrlEncoder(url, text, 1) == 2 && Equals(text, "-w"), "base64url leaves out the padding"); Require(sceNpBase64UrlEncoder(url, text, 2) == 3 && Equals(text, "-wA"), "base64url writes three characters for two bytes"); Require(sceNpBase64UrlGetDecodeSize("-wA", 3) == 2 && sceNpBase64UrlGetDecodeSize("-w==", 4) == 1, "base64url accepts padded and unpadded tails"); Require(sceNpBase64UrlGetDecodeSize("", 0) == NpInvalidCharacter && sceNpBase64UrlGetDecodeSize("AAAAA", 5) == NpInvalidCharacter, "a single trailing character cannot be decoded"); Require(sceNpBase64UrlGetDecodeSize("+w==", 4) == NpInvalidCharacter, "base64url refuses the standard alphabet"); Require(sceNpBase64UrlDecoder("-wA", 3, bytes, 2) == 2 && bytes[0] == 0xFB && bytes[1] == 0, "base64url decodes a short tail"); Require(sceNpBase64UrlDecoder("-w", 2, bytes, 0) == NpInvalidFormat, "base64url decoding refuses a short buffer"); } void TestNumbers() { Require(sceNpHexToInt('a') == 10 && sceNpHexToInt('F') == 15 && sceNpHexToInt('g') == -1, "hex digits are read"); Require(sceNpIntToHex(10, false) == 'a' && sceNpIntToHex(15, true) == 'F' && sceNpIntToHex(9, true) == '9', "hex digits are written"); char text[32] = {}; std::size_t length = 0; Require(sceNpUInt32ToStr(text, sizeof(text), 255, 16, 3, &length) == 0 && Equals(text, "0XFF") && length == 4, "the hex prefix follows the case flag"); Require(sceNpUInt32ToStr(text, sizeof(text), 8, 8, 2, &length) == 0 && Equals(text, "010") && length == 3, "octal gets a zero prefix"); Require(sceNpUInt32ToStr(text, sizeof(text), 0, 10, 4, &length) == 0 && Equals(text, "0") && length == 1, "zero has no sign"); Require(sceNpUInt32ToStr(text, 4, 1234, 10, 0, &length) == NpBufferTooSmall, "the terminator must fit"); Require(sceNpUInt32ToStr(text, sizeof(text), 1, 37, 0, &length) == NpInvalidArgument, "the radix is limited to 36"); Require(sceNpUInt32ToStr(nullptr, 0, 42, 10, 4, &length) == 0 && length == 3, "a null buffer reports the length"); Require(sceNpInt32ToStr(text, sizeof(text), -2147483647 - 1, 10, 0, &length) == 0 && Equals(text, "-2147483648"), "a negative 32-bit value always has a sign"); Require(sceNpInt32ToStr(text, 3, -42, 10, 0, &length) == NpBufferTooSmall, "the sign takes buffer space"); Require(sceNpInt64ToStr(text, sizeof(text), -42, 10, 0, &length) == 0 && Equals(text, "42") && length == 2, "a 64-bit value only gets a sign with the sign flag"); Require(sceNpInt64ToStr(text, sizeof(text), -42, 10, 4, &length) == 0 && Equals(text, "-42"), "the sign flag writes the minus"); Require(sceNpUInt64ToStr(text, sizeof(text), UINT64_MAX, 16, 3, &length) == 0 && Equals(text, "0XFFFFFFFFFFFFFFFF") && length == 18, "the full 64-bit range is formatted"); std::int32_t value32 = 0; std::size_t parsed = 0; Require(sceNpStrToInt32("-567abc", &value32, 10, &parsed) == 0 && value32 == -567 && parsed == 4, "parsing stops at the first non-digit"); Require(sceNpStrToInt32("0x1A", &value32, 0, &parsed) == 0 && value32 == 26, "base zero follows the prefix"); Require(sceNpStrToInt32("invalid", &value32, 10, &parsed) == NpOutOfRange, "no digits is an error"); parsed = 0; Require(sceNpStrToInt32("3000000000", &value32, 10, &parsed) == NpOutOfRange && parsed == 10, "the length is reported before the 32-bit range check"); std::int64_t value64 = 0; Require(sceNpStrToInt64("-9223372036854775808", &value64, 10, nullptr) == 0 && value64 == INT64_MIN, "the 64-bit minimum parses"); Require(sceNpStrToInt64("99999999999999999999", &value64, 10, nullptr) == NpOutOfRange, "64-bit overflow is an error"); std::uint32_t unsigned32 = 0; Require(sceNpStrToUInt32("4294967295", &unsigned32, 10, nullptr) == 0 && unsigned32 == UINT32_MAX, "the 32-bit maximum parses"); Require(sceNpStrToUInt32("4294967296", &unsigned32, 10, nullptr) == NpOutOfRange, "32-bit unsigned overflow is an error"); std::uint64_t unsigned64 = 0; Require(sceNpStrToUInt64("0xFFFFFFFFFFFFFFFF", &unsigned64, 0, nullptr) == 0 && unsigned64 == UINT64_MAX, "the 64-bit maximum parses"); Require(sceNpStrToUInt64("18446744073709551616", &unsigned64, 10, nullptr) == NpOutOfRange, "64-bit unsigned overflow is an error"); } void TestHexAndCopies() { const std::uint8_t data[] = {0x0A, 0xFE}; char text[8] = {}; Require(sceNpStrBuildHex(text, 5, data, 2, false) == 0 && Equals(text, "0afe"), "bytes are written as lowercase hex"); Require(sceNpStrBuildHex(text, 5, data, 2, true) == 0 && Equals(text, "0AFE"), "bytes are written as uppercase hex"); Require(sceNpStrBuildHex(text, 4, data, 2, false) == NpBufferTooSmall, "hex output needs room for the terminator"); std::uint8_t bytes[4] = {}; Require(sceNpStrParseHex("0aFE", bytes, 2) == 0 && bytes[0] == 0x0A && bytes[1] == 0xFE, "hex is read in either case"); Require(sceNpStrParseHex("0AF", bytes, 2) == NpInvalidArgument, "odd hex is refused"); Require(sceNpStrParseHex("0AFE", bytes, 1) == NpBufferTooSmall, "hex parsing checks the output size"); Require(sceNpStrParseHex("0AG1", bytes, 2) == NpInvalidArgument, "non-hex characters are refused"); Require(sceNpStrnParseHex("DEADBEEF1234", 8, bytes, 4) == 0 && bytes[0] == 0xDE && bytes[3] == 0xEF, "a hex prefix is read"); char copy[8] = {}; Require(sceNpStrcpyToBuf(copy, 6, "Hello") == 0 && Equals(copy, "Hello"), "a string that fits is copied"); Require(sceNpStrcpyToBuf(copy, 5, "Hello") == NpBufferTooSmall, "a string without room for its terminator is refused"); Require(sceNpStrncpyToBuf(copy, 6, "Hello World", 5) == 0 && Equals(copy, "Hello"), "the length limit sizes the copy"); Require(sceNpStrncpyToBuf(copy, 5, "Hello World", 5) == NpBufferTooSmall, "the limited string still needs a terminator"); } } int main() { TestAtomics(); TestSemaphoresAndFlags(); TestSharedEventFlags(); TestThreads(); TestHeaps(); TestBase64(); TestNumbers(); TestHexAndCopies(); TestCallouts(); TestIds(); TestLanguagesAndDebugIds(); TestJsonText(); TestTitleIds(); std::puts("guest np common runtime: PASS"); }