diff --git a/core/libs/prx/libc/src/FormattingWide.cpp b/core/libs/prx/libc/src/FormattingWide.cpp index 03591ae318..2c4ddb67bd 100644 --- a/core/libs/prx/libc/src/FormattingWide.cpp +++ b/core/libs/prx/libc/src/FormattingWide.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/Api.hpp" #include #include +#include #include #include #include @@ -172,10 +173,25 @@ std::u16string FormatWide(const char16_t* format, VaList* source, bool secure = spec = spec.substr(0, spec.find('.')) + ".1"; AppendNumber(out, spec + "ll" + static_cast(conversion), value); } else if (In(conversion, "aAeEfFgG")) { - if (length == "L") AppendNumber(out, spec + "L" + static_cast(conversion), args.Next()); - else { + const char narrow = static_cast(conversion); + const bool hexadecimal = narrow == 'a' || narrow == 'A'; + if (length == "L") { + const long double value = args.Next(); + if (!hexadecimal && std::isfinite(value)) { + const std::string text = LibcDetail::DecimalFloat(spec, narrow, value); + out.append(text.begin(), text.end()); + } else { + AppendNumber(out, spec + "L" + narrow, value); + } + } else { if (!length.empty() && length != "l") throw std::invalid_argument("Invalid floating length"); - AppendNumber(out, spec + static_cast(conversion), args.Next()); + const double value = args.Next(); + if (std::isfinite(value)) { + const std::string text = hexadecimal ? LibcDetail::HexFloat(spec, narrow, value) : LibcDetail::DecimalFloat(spec, narrow, value); + out.append(text.begin(), text.end()); + } else { + AppendNumber(out, spec + narrow, value); + } } } else if (conversion == u'p' && length.empty()) { const std::string text = LibcDetail::FormatPointer(spec, args.Next()); diff --git a/core/libs/tests/GuestStdioVariadic.cpp b/core/libs/tests/GuestStdioVariadic.cpp index 8a0f65449d..80d9e2d166 100644 --- a/core/libs/tests/GuestStdioVariadic.cpp +++ b/core/libs/tests/GuestStdioVariadic.cpp @@ -182,7 +182,7 @@ int main() { FileStream written(std::tmpfile()); Require(fwprintf_nid_postfix(&written, u"%d:%ls|", 7, u"été") == 6, "fwprintf counts wide characters"); Require(PrintWide(&written, u"%5.1f", 2.25) == 5, "vfwprintf counts wide characters"); - Require(Contents(written) == "7:\xc3\xa9t\xc3\xa9| 2.2", "the stream holds the formatted text"); + Require(Contents(written) == "7:\xc3\xa9t\xc3\xa9| 2.3", "the stream holds the formatted text"); written.Close(); FileStream securePlain(std::tmpfile()); diff --git a/core/libs/tests/GuestWideFormatting.cpp b/core/libs/tests/GuestWideFormatting.cpp index 56b278d8ae..a317dcfb9e 100644 --- a/core/libs/tests/GuestWideFormatting.cpp +++ b/core/libs/tests/GuestWideFormatting.cpp @@ -177,6 +177,17 @@ static void CheckSecureWideBoundary() { "vsnwprintf_s oversized size leaves the buffer untouched"); } +static void CheckFloat() { + char16_t buffer[64]; + Require(swprintf_nid_postfix(buffer, 64, u"[%.0f|%.1f|%#.3g|%.20f]", 2.5, 0.25, 2025.0, 0.1) == 39 && + buffer == std::u16string(u"[3|0.3|2.03e+03|0.10000000000000000555]"), "swprintf rounds and caps decimal digits"); + Require(swprintf_nid_postfix(buffer, 64, u"[%.0f|%-8.2e]", 67108865.5, 1125.0) == 19 && buffer == std::u16string(u"[67108865|1.13e+03]"), + "swprintf rounds a large tie down"); + Require(swprintf_nid_postfix(buffer, 64, u"[%a|%.3a]", 0x1p-1074, 0x0.fffffffffffffp-1022) == 25 && + buffer == std::u16string(u"[0x1p-1074|0x2.000p-1023]"), "swprintf normalises subnormal hexadecimal floats"); + Require(swprintf_nid_postfix(buffer, 64, u"[%.2Lf]", 0.125L) == 6 && buffer == std::u16string(u"[0.13]"), "swprintf long double rounding"); +} + static void CheckSwprintf() { char16_t buffer[8]; for (auto& unit : buffer) unit = u'x'; @@ -302,6 +313,7 @@ int main() { CheckPointer(); CheckCount(); CheckSwprintf(); + CheckFloat(); CheckSecureWideBoundary(); CheckWcsftime(); Check(u"%.2s", "\xc3\xa9\xc3\xa8", u"\u00e9\u00e8"); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 47f6526c96..51e896297c 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -150,7 +150,7 @@ - [c16rtomb, c32rtomb, mbrtoc16 and mbrtoc32](../../../core/libs/prx/libc/src/Multibyte.cpp) (libSceLibcInternal, implemented in libc) - a null state, which the console dereferences, throws. Otherwise they follow the console's `libSceLibcInternal.sprx` (0x7D480 to 0x7D920), read from disassembly: UTF-8 whatever the locale, up to six bytes per sequence, with no surrogate or 0x10FFFF check when encoding. The state holds the value in its first eight bytes and the expected byte count (0x10 for a pending low surrogate, 1 for a stored high one) as 16 bits at 0xA. `mbrtoc16` builds a low surrogate from the value the state last held, `c16rtomb` returns -1 without `errno` for an unpaired high surrogate, and `EILSEQ` is 86. On a PS5 (13.60) the system library's default locale converts UTF-8 in `wcrtomb`, `wctomb`, `mbrtowc` and `mblen`; a title's bundled `libc.prx` does too until `setlocale` installs a table ([disassembly](https://github.com/boykopovar/AnyPS5/pull/8855#issuecomment-6335267758)) - [The narrow printf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) follows a payload process on a PS5 (firmware 13.60): each 16-bit unit of a `%ls` or `%lc` argument is encoded alone, so a surrogate pair gives two 3-byte sequences; a NaN keeps its sign and the `+` and space flags, and `0` pads it with spaces; argument positions above 99 work (up to 400 measured; above 65535 throw here); an unknown conversion such as `%m` or `%k` prints `%` and its letter, padded to the width, without taking an argument. The guest's `wcrtomb` and locales are not used. On Linux the formats without those conversions or a floating-point conversion go to glibc, which prints NaN the same way. The `_s` forms throw for an unknown conversion, `%m` included, which was not measured - [%a and %A](../../../core/libs/prx/libc/include/WindowsFormatting.hpp) (libc) format a finite double as the `libc.prx` that titles ship does, instead of through the host: a subnormal is normalised (`0x1p-1074`), a shorter precision rounds half up on the dropped hexadecimal digits and a carry stays in the leading digit (`%.3a` of `0x0.fffffffffffffp-1022` is `0x2.000p-1023`), and `-` overrides `0`. Checked by running the copies of PPSA13934, PPSA07923 and PPSA05916 side by side on 7 million random formats, not measured on a console. Infinities and `long double` still go to the host; NaN keeps its sign and flags through the formatter on current main. -- [%e, %f and %g](../../../core/libs/prx/libc/include/WindowsFormatting.hpp) (libc) format a finite `double` or `long double` as the `libc.prx` that titles ship does, instead of through the host. It keeps the exact decimal digits down to position `4 * floor(k / 4) - 20`, where `k` is the `frexp` exponent times 30103 / 100000 truncated, and prints zeros after them; a shorter precision rounds half up on the kept digits, except that an exact tie rounds down when `k` is at least 8 (`%.0f` of 67108865.5 is `67108865`). The console computes the kept digits in extended precision, so about 7% of outputs longer than 17 significant digits end one unit lower than here (`%f` of 1e22 is `9999999999999999999999.000000`). Checked by running the copies of PPSA07923, PPSA05916 and PPSA13934 side by side, not measured on a console. The wide printf family still formats through the host. +- [%e, %f and %g](../../../core/libs/prx/libc/include/WindowsFormatting.hpp) (libc) format a finite `double` or `long double` as the `libc.prx` that titles ship does, instead of through the host. It keeps the exact decimal digits down to position `4 * floor(k / 4) - 20`, where `k` is the `frexp` exponent times 30103 / 100000 truncated, and prints zeros after them; a shorter precision rounds half up on the kept digits, except that an exact tie rounds down when `k` is at least 8 (`%.0f` of 67108865.5 is `67108865`). The console computes the kept digits in extended precision, so about 7% of outputs longer than 17 significant digits end one unit lower than here (`%f` of 1e22 is `9999999999999999999999.000000`). Checked by running the copies of PPSA07923, PPSA05916 and PPSA13934 side by side, not measured on a console. The wide printf family uses the same digits, as the titles' `swprintf` gives the same text as their `snprintf`. - [%p and %lp](../../../core/libs/prx/libc/include/WindowsFormatting.hpp) (libc) follow the console's `libSceLibcInternal.sprx` measured from a homebrew payload on firmware 12.70 ([#7549](https://github.com/boykopovar/AnyPS5/issues/7549)), not FreeBSD: bare lowercase hex with no `0x` prefix even under `#`, `0` for a null pointer, `+` and the space flag ignored, the width counting only the digits, `-` padding on the right, `0` padding with zeros when there is no precision, the precision as the minimum number of digits, nothing for a null pointer under `%.0p`, and `l` accepted. Only `snprintf` and `swprintf` of that module were measured, from a payload process rather than a title process, so a width beside `%.0p`, the `0` flag beside a precision (which C17 ignores) and `%.0p` of a non-null pointer follow this formatter's own rules and are not measured; `swprintf` was not run with `%lp`, so the wide family still refuses that conversion and returns -1. Linux routes `%p` and `%lp` to this formatter because glibc prints the FreeBSD form and `(nil)` for a null pointer, which means a format that combines `%p` with a conversion this formatter refuses, such as an argument position above 99, throws there instead of reaching glibc - [collate and collate _Getcat](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) build the facet for the C locale only and throw for any other locale name, the wide entry point following the narrow one it sits next to. The wide facet carries its own vtable, whose `compare`, `transform` and `hash` take the guest's 16-bit characters, so the length of a range is a count of characters: `compare` orders by 16-bit value (the C locale, as Dinkumware's `char_traits::compare` does), `transform` copies the characters into the result and records size and capacity in 16-bit elements (inline capacity 7, heap capacity `length | 7`, `(capacity + 1) * 2` bytes including the terminator), following Dinkumware's [VC12 `basic_string`](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/xstring#L500-L531), and `hash` mixes one whole character per round, so wide text below 0x100 hashes like its narrow spelling and a character above 0xff hashes as one value instead of the two bytes it holds. The console's `libc.prx` exports both `_Getcat` (NIDs BSVJqITGCyI and 2wz4rthdiy8) with identical 0x19d-byte bodies, believed to build the same 0x20-byte facet; the console bodies themselves are unreadable (obfuscated dynsym, and the text of the relinked `libc.prx` is zeroed), so the wide entries follow the narrow algorithm and the C locale instead of the console's wide code, and no console run checks the two. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size. The narrow result capacity (`length | 15`) and the over-aligned storage of results of 4080 characters or more follow the console's `libc.prx` code and are not checked on hardware. No module of the PPSA59322, PPSA76677 or PPSA97358 dumps in this tree imports either `_Getcat`, so neither has been exercised by a title. - [codecvt, numpunct, num_put and time_put _Getcat](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) build facets matching a bundled console `libc.prx` (dynsym decoded by NID, vtables disassembled): categories 2, 4, 4 and 5, sizes 0x10, 0x30, 0x10 and 0xb8. `codecvt` reproduces the console virtuals (`noconv` conversions, `length` the shorter range); `numpunct` copies its C-locale table (`""`, `.`, `,`, `false`, `true`). `num_put`/`time_put` formatting virtuals throw rather than fabricate output, and `time_put`'s `_Locinfo` table is unpopulated — the console's `do_put` bodies were not mirrored. The facets share the no-op `destroy`/`deleteObject`/`retain`/`release` base used for `collate`, so `numpunct`'s static C-locale strings are never freed. Unlike `collate`, these `_Getcat` entries ignore the locale argument and always build the C-locale facet; the console's behaviour for a non-C name is unverified. `std::clog` and `std::wclog` are 0x400-byte zeroed objects like the rest of the `NSTDSTR` stream set (the console's `libc.prx` sizes `std::clog` at 96 bytes, measured from its dynsym `st_size`), and the `numpunct::id` and `time_put::id` objects are 8-byte zeros like the other facet ids.