diff --git a/core/libs/prx/libc/src/Time.cpp b/core/libs/prx/libc/src/Time.cpp index 1de62e3ee5f3..b62940948f41 100644 --- a/core/libs/prx/libc/src/Time.cpp +++ b/core/libs/prx/libc/src/Time.cpp @@ -5,6 +5,9 @@ #include #include #include +#include +#include +#include #include #ifdef _WIN32 @@ -238,6 +241,9 @@ extern "C" { GuestTm* APS5_VABI localtime_s_nid_postfix(const int64_t* timer, GuestTm* result); GuestTm* APS5_VABI gmtime_s_nid_postfix(const int64_t* timer, GuestTm* result); +std::size_t APS5_VABI wcsrtombs_nid_postfix(char* destination, const std::uint16_t** source, std::size_t capacity, void* state); +std::size_t APS5_VABI mbsrtowcs_nid_postfix(std::uint16_t* destination, const char** source, std::size_t capacity, void* state); +int APS5_VABI ___mb_cur_max_nid_postfix(); void APS5_VABI tzset_nid_postfix(void) { #ifdef _WIN32 @@ -394,6 +400,42 @@ size_t APS5_VABI strftime_nid_postfix(char* str, size_t count, const char* forma return libc_strftime_nid_postfix(str, count, format, timeptr); } +size_t APS5_VABI wcsftime_nid_postfix(std::uint16_t* str, size_t count, const std::uint16_t* format, const GuestTm* timeptr) { + constexpr int GuestEnomem = 12; + constexpr int GuestEinval = 22; + constexpr auto Failed = static_cast(-1); + if (format == nullptr) throw std::invalid_argument("wcsftime: null format"); + if (timeptr == nullptr) throw std::invalid_argument("wcsftime: null time"); + const std::uint16_t* formatCursor = format; + const std::size_t formatLength = wcsrtombs_nid_postfix(nullptr, &formatCursor, 0, nullptr); + if (formatLength == Failed) return 0; + const std::unique_ptr narrowFormat(new (std::nothrow) char[formatLength + 1]); + if (!narrowFormat) { + errno = GuestEnomem; + return 0; + } + wcsrtombs_nid_postfix(narrowFormat.get(), &formatCursor, formatLength + 1, nullptr); + const auto maxCharacterBytes = static_cast(___mb_cur_max_nid_postfix()); + if (std::numeric_limits::max() / maxCharacterBytes <= count) { + errno = GuestEinval; + return 0; + } + const std::unique_ptr narrow(new (std::nothrow) char[count * maxCharacterBytes]); + if (!narrow) { + errno = GuestEnomem; + return 0; + } + const int callerError = errno; + if (strftime_nid_postfix(narrow.get(), count, narrowFormat.get(), timeptr) == 0) { + errno = callerError; + return 0; + } + const char* narrowCursor = narrow.get(); + const std::size_t length = mbsrtowcs_nid_postfix(str, &narrowCursor, count, nullptr); + if (length == static_cast(-2) || length == Failed || narrowCursor != nullptr) return 0; + return length; +} + // The guest's CLOCKS_PER_SEC is 1000000: clock() reports process CPU time in microseconds. int64_t APS5_VABI clock_nid_postfix() { #ifdef _WIN32 diff --git a/core/libs/tests/GuestWideFormatting.cpp b/core/libs/tests/GuestWideFormatting.cpp index b5bb38d0fe24..adba600ad6cc 100644 --- a/core/libs/tests/GuestWideFormatting.cpp +++ b/core/libs/tests/GuestWideFormatting.cpp @@ -1,14 +1,19 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/GuestTime.hpp" #include #include #include #include +#include #include extern "C" int APS5_VABI vswprintf_nid_postfix(char16_t*, std::size_t, const char16_t*, VaList*); extern "C" int APS5_VABI snwprintf_s_nid_postfix(char16_t*, std::size_t, const char16_t*, ...); extern "C" int APS5_VABI swprintf_nid_postfix(char16_t*, std::size_t, const char16_t*, ...); +extern "C" std::size_t APS5_VABI strftime_nid_postfix(char*, std::size_t, const char*, const GuestTm*); +extern "C" std::size_t APS5_VABI wcsftime_nid_postfix(char16_t*, std::size_t, const char16_t*, const GuestTm*); +extern "C" int* APS5_VABI __error_nid_postfix(); static int APS5_VABI Format(char16_t* buffer, std::size_t size, const char16_t* format, ...) { #ifdef _WIN32 @@ -167,12 +172,79 @@ static void CheckSwprintf() { Require(swprintf_nid_postfix(wide, 16, u"%q") < 0 && wide[0] == 0, "swprintf invalid conversion"); } +static bool WcsftimeThrows(char16_t* buffer, const char16_t* format, const GuestTm* time) { + try { + wcsftime_nid_postfix(buffer, 16, format, time); + } catch (const std::invalid_argument&) { + return true; + } + return false; +} + +static void CheckWcsftime() { + GuestTm time{}; + time.tm_sec = 9; + time.tm_min = 5; + time.tm_hour = 13; + time.tm_mday = 29; + time.tm_mon = 1; + time.tm_year = 124; + time.tm_wday = 4; + time.tm_yday = 59; + time.tm_zone = "UTC"; + struct Case { + const char16_t* wide; + const char* narrow; + }; + const Case cases[] = { + {u"%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M:%S"}, + {u"%a %b %j %p %Z", "%a %b %j %p %Z"}, + {u"%%", "%%"}, + {u"%k|%l|%v|%+", "%k|%l|%v|%+"}, + {u"\u00e9t\u00e9 %Y", "\xe9t\xe9 %Y"}, + }; + for (const auto& item : cases) { + char narrow[128]; + char16_t wide[128]; + const std::size_t length = strftime_nid_postfix(narrow, sizeof(narrow), item.narrow, &time); + Require(length != 0, "wcsftime reference strftime"); + Require(wcsftime_nid_postfix(wide, 128, item.wide, &time) == length, "wcsftime length matches strftime"); + for (std::size_t index = 0; index <= length; ++index) + Require(wide[index] == static_cast(narrow[index]), "wcsftime output matches strftime"); + } + char16_t buffer[32]; + Require(wcsftime_nid_postfix(buffer, 32, u"%Y-%m-%d %H:%M:%S", &time) == 19 + && buffer == std::u16string(u"2024-02-29 13:05:09"), "wcsftime date and time"); + Require(wcsftime_nid_postfix(buffer, 32, u"%a %b %j %p", &time) == 14 + && buffer == std::u16string(u"Thu Feb 060 PM"), "wcsftime names"); + Require(wcsftime_nid_postfix(buffer, 32, u"%%", &time) == 1 && buffer == std::u16string(u"%"), "wcsftime percent"); + Require(wcsftime_nid_postfix(buffer, 20, u"%Y-%m-%d %H:%M:%S", &time) == 19, "wcsftime exact fit"); + + int& error = *__error_nid_postfix(); + for (auto& unit : buffer) unit = u'x'; + error = 5; + Require(wcsftime_nid_postfix(buffer, 19, u"%Y-%m-%d %H:%M:%S", &time) == 0 && error == 5 && buffer[0] == u'x', + "wcsftime overflow"); + Require(wcsftime_nid_postfix(buffer, 1, u"%Y", &time) == 0 && error == 5 && buffer[0] == u'x', "wcsftime one unit"); + Require(wcsftime_nid_postfix(buffer, 0, u"%Y", &time) == 0 && error == 5 && buffer[0] == u'x', "wcsftime zero size"); + Require(wcsftime_nid_postfix(buffer, 32, u"", &time) == 0 && error == 5 && buffer[0] == u'x', "wcsftime empty format"); + Require(wcsftime_nid_postfix(nullptr, 32, u"%Y", &time) == 0 && error == 5, "wcsftime null buffer"); + Require(wcsftime_nid_postfix(buffer, 32, u"%Y \u20ac", &time) == 0 && error == 86 && buffer[0] == u'x', + "wcsftime unconvertible format"); + error = 5; + Require(wcsftime_nid_postfix(buffer, SIZE_MAX, u"%Y", &time) == 0 && error == 22 && buffer[0] == u'x', + "wcsftime oversized buffer"); + Require(WcsftimeThrows(buffer, nullptr, &time), "wcsftime null format"); + Require(WcsftimeThrows(buffer, u"%Y", nullptr), "wcsftime null time"); +} + int main() { CheckBounded(); CheckPositional(); CheckPointer(); CheckCount(); CheckSwprintf(); + CheckWcsftime(); Check(u"%.2s", "\xc3\xa9\xc3\xa8", u"\u00e9\u00e8"); Check(u"%.1s", "\xc3\xa9\xc3\xa8", u"\u00e9"); Check(u"%.0s", "\xc3\xa9", u""); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 35299259dad0..75a922ae304b 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -53,6 +53,7 @@ - [Guest path resolution](../../../core/libs/prx/libc/src/General.cpp) falls back to ASCII case-insensitive component lookup, following [shadPS4's host filesystem](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/file_sys/backends/host_fs.cpp). PPSA03747 requests `Skyrim.INI`, `Data/Video` and lowercase archive/plugin names that differ from the dump's filenames. Exact host names take precedence; multiple fallback matches throw instead of selecting an arbitrary file. Existing parent names are resolved when creating a new file, and guest mount aliases use the same ASCII comparison. Results are not cached, so renames and newly created files remain visible. Unicode case folding and PS5 behaviour for case-colliding host entries are unverified; host roots are used as supplied. Explicit native Windows drive and UNC paths bypass guest parent-component validation. - [localtime, mktime](../../../core/libs/prx/libc/src/Time.cpp) (libc) on Windows convert through the CRT's `localtime_s` and `mktime`, which accept about 1970 to January 3001 UTC and apply the CRT's daylight-saving rule. A time outside that range is converted a whole number of 400-year Gregorian cycles away, where the calendar repeats, so it gets the rule the CRT applies to that year and not the zone's historical one. `gmtime` converts every time whose year fits an `int` on both hosts. - [strftime](../../../core/libs/prx/libc/src/Time.cpp) (libc) expands FreeBSD's `%k`, `%l`, `%s`, `%v` and `%+` itself, ignoring a leading `E` or `O` as FreeBSD does. Unknown conversions, repeated modifiers and incomplete conversions emit literals as in FreeBSD 11; recognized conversions otherwise go to the host's `strftime`. On Windows the `%-`, `%_` and `%0` padding flags still produce nothing, including on the five expanded conversions (`%-k`, `%_k` and `%0k` go to the host), and a standalone `%z` or `%Z` uses the host's zone instead of the broken-down time's `tm_zone`. `%s` follows FreeBSD 9 and 11 and is the local-time `mktime` of the broken-down time (current FreeBSD uses `tm_gmtoff`), which is -1 on Windows outside the range above. `%+` is the C locale's `%a %b %e %H:%M:%S %Z %Y` with `%Z` taken from `tm_zone`, or from `tzname[tm_isdst != 0]` when it is null. +- [wcsftime](../../../core/libs/prx/libc/src/Time.cpp) (libc) follows FreeBSD 11.4's [wcsftime.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/locale/wcsftime.c) over this library's `wcsrtombs`, `strftime` and `mbsrtowcs` in the single-byte C locale of [none.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/locale/none.c), with the guest's 16-bit `wchar_t`, without console verification. It has the `strftime` gaps above. A format unit above 255 returns 0 with `EILSEQ`; an empty result, a result that does not fit and a null buffer return 0 with `errno` unchanged, discarding the `ERANGE` or `EINVAL` that the Windows CRT's `strftime` sets. A null format or `tm` throws, where FreeBSD dereferences it (a null `tm` is read there only when the format has a conversion that needs it) - [syslog](../../../core/libs/prx/libc/src/Syslog.cpp) (libc) writes each message to `stderr` as `[syslog:] `, with a newline added when missing, instead of sending it to the console's system log. Priority and facility handling follow FreeBSD's `vsyslog`: `%m` expands to the guest errno's message, unknown bits are reported (as priority 0x23, since FreeBSD passes `LOG_ERR|LOG_CONS|LOG_PERROR|LOG_PID` as the priority) and masked, a missing facility becomes `LOG_USER`, and errno is preserved. `setlogmask` follows [FreeBSD 11](https://github.com/freebsd/freebsd-src/blob/8d2708de5d6112a492744d367cfdfb8fd219227a/lib/libc/gen/syslog.c): the process-wide mask defaults to 0xff, zero queries it, and nonzero values replace it; masked messages are skipped before formatting, including the unknown-priority diagnostic. The console's mask behaviour is unverified. `openlog` and `closelog` are not implemented, so their identity and options are not applied - [daemon](../../../core/libs/prx/libc/src/Process.cpp) (libc) throws: forking into the background is not supported. PPSA85300 imports it and fails to load without it. VLC 3.0 calls it only when its `daemon` option is set ([libvlc.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/libvlc.c#L190-L197)), and the title starts libVLC with `--ignore-config` and without `--daemon` ([player.cc](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/src/player.cc#L960-L981)) - [_WStoul](../../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../../core/libs/prx/libc/src/RuneLocale.cpp) and [swscanf_s](../../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert