From 3515ead91a7c28dd51b0f3d79cbbd50bc129b9b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 07:27:27 +0200 Subject: [PATCH] feat(rtc): implement sceRtcFormatRFC2822 and the RFC 2822/3339 local-time formatters Co-Authored-By: Claude --- core/libs/prx/libSceRtc/Export.cpp | 29 +++++++++++++++ core/libs/tests/GuestRtc.cpp | 60 ++++++++++++++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 90 insertions(+) diff --git a/core/libs/prx/libSceRtc/Export.cpp b/core/libs/prx/libSceRtc/Export.cpp index 08bf4bb06..09a4ccbe6 100644 --- a/core/libs/prx/libSceRtc/Export.cpp +++ b/core/libs/prx/libSceRtc/Export.cpp @@ -107,6 +107,13 @@ std::int64_t localOffsetSeconds(std::uint64_t utcTick) { return localSeconds - static_cast(seconds); } +constexpr const char* WEEKDAY_NAMES[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; +constexpr const char* MONTH_NAMES[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + +int localOffsetMinutes(const RtcTick& utc) { + return static_cast(localOffsetSeconds(utc.tick) / 60); +} + int addTicks(RtcTick* dst, const RtcTick* src, std::int64_t count, std::int64_t unit) { if (!dst || !src) return SCE_RTC_ERROR_INVALID_POINTER; if (src->tick > MAX_TICK) return SCE_RTC_ERROR_INVALID_VALUE; @@ -286,6 +293,28 @@ int APS5_VABI sceRtcFormatRFC3339(char* date_time, const RtcTick* utc, int time_ return 0; } +int APS5_VABI sceRtcFormatRFC3339LocalTime(char* date_time, const RtcTick* utc) { + if (!date_time || !utc) return SCE_RTC_ERROR_INVALID_POINTER; + return sceRtcFormatRFC3339(date_time, utc, localOffsetMinutes(*utc)); +} + +int APS5_VABI sceRtcFormatRFC2822(char* date_time, const RtcTick* utc, int time_zone_minutes) { + if (!date_time || !utc) return SCE_RTC_ERROR_INVALID_POINTER; + if (time_zone_minutes < -1439 || time_zone_minutes > 1439) return SCE_RTC_ERROR_INVALID_VALUE; + RtcTick local{}; + if (const int result = addTicks(&local, utc, time_zone_minutes, TICKS_PER_MINUTE); result != 0) return result; + const RtcDateTime time = fromTick(local.tick); + const int offset = time_zone_minutes < 0 ? -time_zone_minutes : time_zone_minutes; + std::snprintf(date_time, 32, "%s, %02u %s %04u %02u:%02u:%02u %c%02d%02d", WEEKDAY_NAMES[(local.tick / TICKS_PER_DAY + 1) % 7], time.day, + MONTH_NAMES[time.month - 1], time.year, time.hour, time.minute, time.second, time_zone_minutes < 0 ? '-' : '+', offset / 60, offset % 60); + return 0; +} + +int APS5_VABI sceRtcFormatRFC2822LocalTime(char* date_time, const RtcTick* utc) { + if (!date_time || !utc) return SCE_RTC_ERROR_INVALID_POINTER; + return sceRtcFormatRFC2822(date_time, utc, localOffsetMinutes(*utc)); +} + int APS5_VABI sceRtcParseRFC3339(RtcTick* utc, const char* date_time) { if (!utc || !date_time) return SCE_RTC_ERROR_INVALID_POINTER; const char* cursor = date_time; diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index 2d843ebfe..8f8bdcf08 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -30,10 +31,23 @@ int APS5_VABI sceRtcTickAddSeconds(RtcTick*, const RtcTick*, std::int64_t); int APS5_VABI sceRtcTickAddDays(RtcTick*, const RtcTick*, std::int32_t); int APS5_VABI sceRtcTickAddMonths(RtcTick*, const RtcTick*, std::int32_t); int APS5_VABI sceRtcTickAddYears(RtcTick*, const RtcTick*, std::int16_t); +int APS5_VABI sceRtcFormatRFC3339LocalTime(char*, const RtcTick*); +int APS5_VABI sceRtcFormatRFC2822(char*, const RtcTick*, int); +int APS5_VABI sceRtcFormatRFC2822LocalTime(char*, const RtcTick*); } static void Require(bool value) { if (!value) std::abort(); } +static void SetTimeZone(const char* zone) { +#ifdef _WIN32 + _putenv_s("TZ", zone); + _tzset(); +#else + setenv("TZ", zone, 1); + tzset(); +#endif +} + static bool Equal(const RtcDateTime& left, const RtcDateTime& right) { return left.year == right.year && left.month == right.month && left.day == right.day && left.hour == right.hour && left.minute == right.minute && left.second == right.second && left.microsecond == right.microsecond; @@ -224,4 +238,50 @@ int main() { RtcTick back{}; Require(sceRtcConvertUtcToLocalTime(&now, &local) == 0); Require(sceRtcConvertLocalTimeToUtc(&local, &back) == 0 && back.tick == now.tick); + + char rfc2822[32]; + tick.tick = leapDayTick; + Require(sceRtcFormatRFC2822(rfc2822, &tick, 0) == 0 && std::strcmp(rfc2822, "Thu, 29 Feb 2024 12:34:56 +0000") == 0); + Require(sceRtcFormatRFC2822(rfc2822, &tick, 90) == 0 && std::strcmp(rfc2822, "Thu, 29 Feb 2024 14:04:56 +0130") == 0); + Require(sceRtcFormatRFC2822(rfc2822, &tick, -300) == 0 && std::strcmp(rfc2822, "Thu, 29 Feb 2024 07:34:56 -0500") == 0); + Require(sceRtcFormatRFC2822(rfc2822, &tick, -30) == 0 && std::strcmp(rfc2822, "Thu, 29 Feb 2024 12:04:56 -0030") == 0); + Require(sceRtcFormatRFC2822(rfc2822, &tick, 65) == 0 && std::strcmp(rfc2822, "Thu, 29 Feb 2024 13:39:56 +0105") == 0); + Require(sceRtcFormatRFC2822(rfc2822, &tick, 1439) == 0 && std::strcmp(rfc2822, "Fri, 01 Mar 2024 12:33:56 +2359") == 0); + Require(sceRtcFormatRFC2822(rfc2822, &tick, -1439) == 0 && std::strcmp(rfc2822, "Wed, 28 Feb 2024 12:35:56 -2359") == 0); + const char* week[] = {"Sun, 03 Mar", "Mon, 04 Mar", "Tue, 05 Mar", "Wed, 06 Mar", "Thu, 07 Mar", "Fri, 08 Mar", "Sat, 09 Mar"}; + for (int day = 0; day < 7; ++day) { + const RtcDateTime date{2024, 3, static_cast(3 + day), 0, 0, 0, 0}; + Require(sceRtcGetTick(&date, &tick) == 0 && sceRtcFormatRFC2822(rfc2822, &tick, 0) == 0); + Require(std::strncmp(rfc2822, week[day], std::strlen(week[day])) == 0); + } + tick.tick = 0; + Require(sceRtcFormatRFC2822(rfc2822, &tick, 0) == 0 && std::strcmp(rfc2822, "Mon, 01 Jan 0001 00:00:00 +0000") == 0); + Require(sceRtcFormatRFC2822(rfc2822, &tick, -1) == invalidValue); + tick.tick = maxTick; + Require(sceRtcFormatRFC2822(rfc2822, &tick, 0) == 0 && std::strcmp(rfc2822, "Fri, 31 Dec 9999 23:59:59 +0000") == 0); + Require(sceRtcFormatRFC2822(rfc2822, &tick, 1) == invalidValue); + tick.tick = leapDayTick; + for (int offset : {1440, -1440, std::numeric_limits::min(), std::numeric_limits::max()}) { + std::memset(rfc2822, 'x', sizeof(rfc2822)); + char original[sizeof(rfc2822)]; + std::memcpy(original, rfc2822, sizeof(rfc2822)); + Require(sceRtcFormatRFC2822(rfc2822, &tick, offset) == invalidValue); + Require(std::memcmp(rfc2822, original, sizeof(rfc2822)) == 0); + } + Require(sceRtcFormatRFC2822(nullptr, &tick, 0) == invalidPointer); + Require(sceRtcFormatRFC2822(rfc2822, nullptr, 0) == invalidPointer); + + SetTimeZone("XXX-5:30"); + Require(sceRtcFormatRFC2822LocalTime(rfc2822, &tick) == 0 && std::strcmp(rfc2822, "Thu, 29 Feb 2024 18:04:56 +0530") == 0); + Require(sceRtcFormatRFC3339LocalTime(text, &tick) == 0 && std::strcmp(text, "2024-02-29T18:04:56.78+05:30") == 0); + SetTimeZone("XXX+3"); + Require(sceRtcFormatRFC2822LocalTime(rfc2822, &tick) == 0 && std::strcmp(rfc2822, "Thu, 29 Feb 2024 09:34:56 -0300") == 0); + Require(sceRtcFormatRFC3339LocalTime(text, &tick) == 0 && std::strcmp(text, "2024-02-29T09:34:56.78-03:00") == 0); + SetTimeZone("UTC0"); + Require(sceRtcFormatRFC2822LocalTime(rfc2822, &tick) == 0 && std::strcmp(rfc2822, "Thu, 29 Feb 2024 12:34:56 +0000") == 0); + Require(sceRtcFormatRFC3339LocalTime(text, &tick) == 0 && std::strcmp(text, "2024-02-29T12:34:56.78Z") == 0); + Require(sceRtcFormatRFC2822LocalTime(nullptr, &tick) == invalidPointer); + Require(sceRtcFormatRFC2822LocalTime(rfc2822, nullptr) == invalidPointer); + Require(sceRtcFormatRFC3339LocalTime(nullptr, &tick) == invalidPointer); + Require(sceRtcFormatRFC3339LocalTime(text, nullptr) == invalidPointer); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index cd0f433c1..cb62288f1 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -118,6 +118,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceSystemServiceReenableMediaPlay](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature - [sceSystemServiceParamGetString](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the system name (parameter 6) is the fixed `PS5`, not the console's name; other parameters and buffers of 1 to 64 bytes throw - [sceVoiceSetMuteFlag](../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceRtcFormatRFC2822, sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - only shadPS4 (PS4) has bodies. A null tick returns `INVALID_POINTER` like the existing `sceRtcFormatRFC3339`, where shadPS4 formats the current time instead; offsets outside ±1439 minutes return `INVALID_VALUE` like `sceRtcFormatRFC3339`; the local-time variants use the host's offset at the given instant (shadPS4 uses `sceKernelGettimezone`, the current offset) - [snwprintf_s](../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follows C11 K.3.9.1.3 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`. A runtime-constraint violation only returns a negative value: `set_constraint_handler_s` is not exported and the console's default handler is unknown. An invalid multibyte `%s` argument is copied byte by byte instead of being reported as an encoding error, as in `vswprintf` - [libSceUlt queues](../../core/libs/prx/libSceUlt/Export.cpp) - the added `sceUltQueuePop`, `sceUltQueueTryPush`, `sceUltQueueDestroy` and `sceUltQueueDataResourcePoolDestroy` signatures are inferred from the existing push/pop and object APIs. Queue destruction is assumed to reject blocked callers with `ULT_ERROR_BUSY` and otherwise discard queued data; pool exhaustion returns `ULT_ERROR_AGAIN`. These rules, alignment checks and shared slot limits have not been confirmed on PS5 hardware or a title. Non-null queue and data-pool option parameters throw because their layouts are unknown. - [_sceUlobjmgrRegisterObject](../../core/libs/prx/ulobjmgr/Export.cpp) (ulobjmgr) - parameters taken from shadPS4 (PS4): a nonzero 64-bit object, a nonzero 32-bit kind and a 32-bit id output; the meaning of the first two and the PS5 signature are unverified. The id bound of `_sceUlobjmgrUnregisterObject` (below 0x4000) and the raw `EINVAL` (22) return also come from shadPS4