From 1d31901c4bf72f3c9bce22b7dabe8d3d71cf16b2 Mon Sep 17 00:00:00 2001 From: verusferro <99999109+verusferro@users.noreply.github.com> Date: Sun, 25 Oct 2026 18:27:49 +0300 Subject: [PATCH] feat(libc): format num_put::do_put as the console's libc does The num_put facet's eight do_put overloads build the console's printf format from the stream flags, format through snprintf and lay out the result as the SDK libc.prx does: thousands separators from the stream locale's numpunct, its decimal point, fill and width with left, right and internal adjustment, true and false names under boolalpha, and the console's prefix and infinity rules for floating point. --- core/libs/prx/libc/include/GuestLocale.hpp | 27 ++- core/libs/prx/libc/src/LocaleSupport.cpp | 209 ++++++++++++++++++++- core/libs/tests/GuestLocale.cpp | 71 +++++++ docs/dev/debt/libc.md | 3 +- 4 files changed, 299 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libc/include/GuestLocale.hpp b/core/libs/prx/libc/include/GuestLocale.hpp index 3d980fa9ca..c7f8883b2d 100644 --- a/core/libs/prx/libc/include/GuestLocale.hpp +++ b/core/libs/prx/libc/include/GuestLocale.hpp @@ -89,11 +89,6 @@ struct NumpunctVtable { String* (APS5_VABI *truename)(String* result, const NumpunctFacet* self); }; -struct UnsupportedMethodsVtable { - FacetVtable facet; - void (APS5_VABI *methods[8])(); -}; - struct IosBase { const void* vtable; std::int64_t standardStream; @@ -133,11 +128,33 @@ struct TimePutVtable { OutputIterator (APS5_VABI *put)(const Facet* self, OutputIterator out, IosBase* stream, char fill, const GuestTm* time, char format, char modifier); }; +struct NumPutVtable { + FacetVtable facet; + OutputIterator (APS5_VABI *putBool)(const Facet* self, OutputIterator out, IosBase* stream, char fill, bool value); + OutputIterator (APS5_VABI *putLong)(const Facet* self, OutputIterator out, IosBase* stream, char fill, std::int64_t value); + OutputIterator (APS5_VABI *putUnsignedLong)(const Facet* self, OutputIterator out, IosBase* stream, char fill, std::uint64_t value); + OutputIterator (APS5_VABI *putLongLong)(const Facet* self, OutputIterator out, IosBase* stream, char fill, std::int64_t value); + OutputIterator (APS5_VABI *putUnsignedLongLong)(const Facet* self, OutputIterator out, IosBase* stream, char fill, std::uint64_t value); + OutputIterator (APS5_VABI *putDouble)(const Facet* self, OutputIterator out, IosBase* stream, char fill, double value); + OutputIterator (APS5_VABI *putLongDouble)(const Facet* self, OutputIterator out, IosBase* stream, char fill, long double value); + OutputIterator (APS5_VABI *putPointer)(const Facet* self, OutputIterator out, IosBase* stream, char fill, const void* value); +}; + +struct CtypeVtable { + FacetVtable facet; + const void* caseMethods[4]; + char (APS5_VABI *widen)(const Facet* self, char character); + const char* (APS5_VABI *widenRange)(const Facet* self, const char* first, const char* last, char* destination); +}; + static_assert(offsetof(StreambufVtable, overflow) == 0x20); static_assert(offsetof(Streambuf, putNext) == 0x40); static_assert(offsetof(Streambuf, putEnd) == 0x60); static_assert(sizeof(OutputIterator) == 0x10); static_assert(offsetof(TimePutVtable, put) == 0x20); +static_assert(offsetof(NumPutVtable, putBool) == 0x20); +static_assert(offsetof(NumPutVtable, putPointer) == 0x58); +static_assert(offsetof(CtypeVtable, widenRange) == 0x48); static_assert(sizeof(Facet) == 0x10); static_assert(offsetof(Implementation, facets) == 0x10); static_assert(offsetof(Implementation, facetCount) == 0x18); diff --git a/core/libs/prx/libc/src/LocaleSupport.cpp b/core/libs/prx/libc/src/LocaleSupport.cpp index 6ac9473092..04e7866403 100644 --- a/core/libs/prx/libc/src/LocaleSupport.cpp +++ b/core/libs/prx/libc/src/LocaleSupport.cpp @@ -1,5 +1,7 @@ #include "prx/libc/include/Api.hpp" #include +#include +#include #include #include #include @@ -25,6 +27,12 @@ #include "prx/libc/include/GuestLocale.hpp" #include "SceTypes.hpp" +extern "C" { +extern std::uint64_t _ZNSt5ctypeIcE2idE_nid_postfix; +extern std::uint64_t _ZNSt8numpunctIcE2idE_nid_postfix; +extern std::int32_t _ZNSt6locale2id7_Id_cntE_nid_postfix; +} + namespace { std::mutex g_localeInitMutex; @@ -281,10 +289,6 @@ GuestLocale::String* APS5_VABI NumpunctTruename(GuestLocale::String* result, con const GuestLocale::NumpunctVtable g_numpunctVtable{{DestroyCollate, DeleteCollate, RetainFacet, ReleaseFacet}, NumpunctDecimalPoint, NumpunctThousandsSep, NumpunctGrouping, NumpunctFalsename, NumpunctTruename}; -void APS5_VABI NumPutMethodUnsupported() { - NotImplemented_nid_no_patch("num_put::do_put"); -} - void PutCharacter(GuestLocale::OutputIterator& out, char character) { GuestLocale::Streambuf* buffer = out.buffer; if (buffer == nullptr) { @@ -312,7 +316,202 @@ GuestLocale::OutputIterator APS5_VABI TimePutDoPut(const GuestLocale::Facet*, Gu return out; } -const GuestLocale::UnsupportedMethodsVtable g_numPutVtable{{DestroyCollate, DeleteCollate, RetainFacet, ReleaseFacet}, {NumPutMethodUnsupported, NumPutMethodUnsupported, NumPutMethodUnsupported, NumPutMethodUnsupported, NumPutMethodUnsupported, NumPutMethodUnsupported, NumPutMethodUnsupported, NumPutMethodUnsupported}}; +constexpr std::int32_t IosLeft = 0x2; +constexpr std::int32_t IosInternal = 0x8; +constexpr std::int32_t IosAdjustField = 0xe; +constexpr std::int32_t IosOct = 0x20; +constexpr std::int32_t IosHex = 0x40; +constexpr std::int32_t IosBaseField = 0x70; +constexpr std::int32_t IosShowBase = 0x80; +constexpr std::int32_t IosShowPoint = 0x100; +constexpr std::int32_t IosUppercase = 0x200; +constexpr std::int32_t IosShowPos = 0x400; +constexpr std::int32_t IosScientific = 0x800; +constexpr std::int32_t IosFixed = 0x1000; +constexpr std::int32_t IosHexFloat = 0x1800; +constexpr std::int32_t IosBoolAlpha = 0x8000; + +const GuestLocale::Facet* FindFacet(const GuestLocale::IosBase* stream, std::uint64_t& id) { + std::atomic_ref slot(id); + if (slot.load() == 0) { + _Locksyslock_nid_postfix(); + if (slot.load() == 0) slot.store(static_cast(++_ZNSt6locale2id7_Id_cntE_nid_postfix)); + _Unlocksyslock_nid_postfix(); + } + const std::uint64_t index = slot.load(); + const GuestLocale::Implementation* locale = *stream->locale; + const GuestLocale::Facet* facet = index < locale->facetCount ? locale->facets[index] : nullptr; + if (facet != nullptr || !locale->transparent) return facet; + const GuestLocale::Implementation* global = _ZNSt6locale16_GetgloballocaleEv_nid_postfix(); + return index < global->facetCount ? global->facets[index] : nullptr; +} + +const GuestLocale::Facet* StreamNumpunct(const GuestLocale::IosBase* stream) { + if (const GuestLocale::Facet* facet = FindFacet(stream, _ZNSt8numpunctIcE2idE_nid_postfix)) return facet; + static GuestLocale::Facet* const fallback = [] { + GuestLocale::Facet* created = nullptr; + _ZNSt8numpunctIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(&created, nullptr); + return created; + }(); + return fallback; +} + +const GuestLocale::NumpunctVtable& Punctuation(const GuestLocale::Facet* facet) { + return *reinterpret_cast(facet->vtable); +} + +const GuestLocale::NumpunctFacet* AsNumpunct(const GuestLocale::Facet* facet) { + return reinterpret_cast(facet); +} + +std::string TakeString(GuestLocale::String* (APS5_VABI* method)(GuestLocale::String*, const GuestLocale::NumpunctFacet*), const GuestLocale::Facet* facet) { + GuestLocale::String value{}; + method(&value, AsNumpunct(facet)); + const bool onHeap = value.capacity > GuestStringInlineCapacity; + std::string text(onHeap ? value.pointer : value.buffer, value.size); + if (onHeap) _ZdlPv_nid_postfix(value.capacity + 1 < GuestStringLargeAllocation ? static_cast(value.pointer) : reinterpret_cast(value.pointer)[-1]); + return text; +} + +std::string Widen(const GuestLocale::IosBase* stream, const char* text, std::size_t count) { + std::string result(text, count); + if (const GuestLocale::Facet* ctype = FindFacet(stream, _ZNSt5ctypeIcE2idE_nid_postfix)) reinterpret_cast(ctype->vtable)->widenRange(ctype, text, text + count, result.data()); + return result; +} + +bool HexPrefix(const char* text, std::size_t prefix, std::size_t count) { + return prefix + 2 <= count && text[prefix] == '0' && (text[prefix + 1] == 'x' || text[prefix + 1] == 'X'); +} + +std::size_t SignPrefix(const char* text, std::size_t count) { + return count > 0 && (text[0] == '+' || text[0] == '-') ? 1 : 0; +} + +void InsertSeparators(std::string& text, const std::string& grouping, char separator, std::size_t offset, std::size_t prefix) { + const char* group = grouping.c_str(); + while (static_cast(*group) != CHAR_MAX && static_cast(*group) > 0 && static_cast(*group) < offset - prefix) { + offset -= static_cast(*group); + text.insert(offset, 1, separator); + if (static_cast(group[1]) > 0) ++group; + } +} + +GuestLocale::OutputIterator PutField(GuestLocale::OutputIterator out, GuestLocale::IosBase* stream, char fill, const std::string& text, std::size_t prefix) { + const std::int32_t width = stream->width; + std::size_t fillCount = width <= 0 || static_cast(width) <= text.size() ? 0 : static_cast(width) - text.size(); + const std::int32_t adjust = stream->flags & IosAdjustField; + if (adjust != IosLeft && adjust != IosInternal) { + for (; fillCount != 0; --fillCount) PutCharacter(out, fill); + } + for (std::size_t index = 0; index < prefix; ++index) PutCharacter(out, text[index]); + if (adjust == IosInternal) { + for (; fillCount != 0; --fillCount) PutCharacter(out, fill); + } + for (std::size_t index = prefix; index < text.size(); ++index) PutCharacter(out, text[index]); + stream->width = 0; + for (; fillCount != 0; --fillCount) PutCharacter(out, fill); + return out; +} + +GuestLocale::OutputIterator PutInteger(GuestLocale::OutputIterator out, GuestLocale::IosBase* stream, char fill, const char* digits, std::size_t count) { + std::size_t prefix = SignPrefix(digits, count); + if ((stream->flags & IosBaseField) == IosHex && HexPrefix(digits, prefix, count)) prefix += 2; + std::string text = Widen(stream, digits, count); + const GuestLocale::Facet* punct = StreamNumpunct(stream); + const std::string grouping = TakeString(Punctuation(punct).grouping, punct); + if (static_cast(grouping[0]) != CHAR_MAX && static_cast(grouping[0]) > 0) InsertSeparators(text, grouping, Punctuation(punct).thousandsSep(AsNumpunct(punct)), count, prefix); + return PutField(out, stream, fill, text, prefix); +} + +GuestLocale::OutputIterator PutFloating(GuestLocale::OutputIterator out, GuestLocale::IosBase* stream, char fill, const char* digits, std::size_t count) { + const std::size_t sign = SignPrefix(digits, count); + const std::size_t prefix = sign != 0 ? sign : HexPrefix(digits, 0, count) ? 2 : 0; + const bool hex = (stream->flags & IosHexFloat) == IosHexFloat; + const std::size_t exponent = std::strcspn(digits, hex ? "pP" : "eE"); + const char point[2] = {localeconv_nid_postfix()->decimal_point[0], '\0'}; + const std::size_t pointOffset = std::strcspn(digits, point); + std::string text = Widen(stream, digits, count); + const GuestLocale::Facet* punct = StreamNumpunct(stream); + const std::string grouping = TakeString(Punctuation(punct).grouping, punct); + const char separator = Punctuation(punct).thousandsSep(AsNumpunct(punct)); + std::size_t offset = exponent; + if (pointOffset != count) { + offset = pointOffset; + text[pointOffset] = Punctuation(punct).decimalPoint(AsNumpunct(punct)); + } + const char lead = digits[prefix]; + if (lead != 'I' && lead != 'N' && lead != 'i' && lead != 'n') InsertSeparators(text, grouping, separator, offset, prefix); + return PutField(out, stream, fill, text, prefix); +} + +template GuestLocale::OutputIterator PutFormattedInteger(GuestLocale::OutputIterator out, GuestLocale::IosBase* stream, char fill, const char* qualifier, char decimal, Value value) { + const std::int32_t flags = stream->flags; + const std::int32_t base = flags & IosBaseField; + const char conversion = base == IosOct ? 'o' : base != IosHex ? decimal : (flags & IosUppercase) != 0 ? 'X' : 'x'; + const std::string format = std::string("%") + ((flags & IosShowPos) != 0 ? "+" : "") + ((flags & IosShowBase) != 0 ? "#" : "") + qualifier + conversion; + char digits[64]; + const int count = snprintf_nid_postfix(digits, sizeof(digits), format.c_str(), value); + return PutInteger(out, stream, fill, digits, static_cast(count)); +} + +template GuestLocale::OutputIterator PutFormattedFloating(GuestLocale::OutputIterator out, GuestLocale::IosBase* stream, char fill, const char* qualifier, Value value) { + const std::int32_t flags = stream->flags; + const std::int32_t field = flags & IosHexFloat; + const std::int32_t precision = field == IosHexFloat ? 6 : stream->precision; + std::int64_t size = precision; + if (field == IosFixed && std::fabs(value) > Value(1e10)) { + int exponent = 0; + std::frexp(value, &exponent); + size += static_cast(std::abs(exponent)) * 30103 / 100000; + } + size += 50; + const char conversion = field == IosFixed ? 'f' : field == IosHexFloat ? 'a' : field == IosScientific ? 'e' : 'g'; + const bool upper = (flags & IosUppercase) != 0 && field != IosFixed; + const std::string format = std::string("%") + ((flags & IosShowPos) != 0 ? "+" : "") + ((flags & IosShowPoint) != 0 ? "#" : "") + (field != IosHexFloat ? ".*" : "") + qualifier + static_cast(upper ? conversion - 'a' + 'A' : conversion); + if (size <= 0) throw std::invalid_argument("num_put::do_put: the precision leaves no room for the converted value"); + std::string digits(static_cast(size), '\0'); + const int count = snprintf_nid_postfix(digits.data(), digits.size(), format.c_str(), precision, value); + if (count < 0 || count >= size) throw std::invalid_argument("num_put::do_put: the precision leaves no room for the converted value"); + return PutFloating(out, stream, fill, digits.c_str(), static_cast(count)); +} + +GuestLocale::OutputIterator APS5_VABI NumPutBool(const GuestLocale::Facet* self, GuestLocale::OutputIterator out, GuestLocale::IosBase* stream, char fill, bool value) { + if ((stream->flags & IosBoolAlpha) == 0) return reinterpret_cast(self->vtable)->putLong(self, out, stream, fill, value ? 1 : 0); + const GuestLocale::Facet* punct = StreamNumpunct(stream); + return PutField(out, stream, fill, TakeString(value ? Punctuation(punct).truename : Punctuation(punct).falsename, punct), 0); +} + +GuestLocale::OutputIterator APS5_VABI NumPutLong(const GuestLocale::Facet*, GuestLocale::OutputIterator out, GuestLocale::IosBase* stream, char fill, std::int64_t value) { + return PutFormattedInteger(out, stream, fill, "l", 'd', value); +} + +GuestLocale::OutputIterator APS5_VABI NumPutUnsignedLong(const GuestLocale::Facet*, GuestLocale::OutputIterator out, GuestLocale::IosBase* stream, char fill, std::uint64_t value) { + return PutFormattedInteger(out, stream, fill, "l", 'u', value); +} + +GuestLocale::OutputIterator APS5_VABI NumPutLongLong(const GuestLocale::Facet*, GuestLocale::OutputIterator out, GuestLocale::IosBase* stream, char fill, std::int64_t value) { + return PutFormattedInteger(out, stream, fill, "ll", 'd', value); +} + +GuestLocale::OutputIterator APS5_VABI NumPutUnsignedLongLong(const GuestLocale::Facet*, GuestLocale::OutputIterator out, GuestLocale::IosBase* stream, char fill, std::uint64_t value) { + return PutFormattedInteger(out, stream, fill, "ll", 'u', value); +} + +GuestLocale::OutputIterator APS5_VABI NumPutDouble(const GuestLocale::Facet*, GuestLocale::OutputIterator out, GuestLocale::IosBase* stream, char fill, double value) { + return PutFormattedFloating(out, stream, fill, "", value); +} + +GuestLocale::OutputIterator APS5_VABI NumPutLongDouble(const GuestLocale::Facet*, GuestLocale::OutputIterator out, GuestLocale::IosBase* stream, char fill, long double value) { + return PutFormattedFloating(out, stream, fill, "L", value); +} + +GuestLocale::OutputIterator APS5_VABI NumPutPointer(const GuestLocale::Facet*, GuestLocale::OutputIterator out, GuestLocale::IosBase* stream, char fill, const void* value) { + char digits[64]; + const int count = snprintf_nid_postfix(digits, sizeof(digits), "%p", value); + return PutInteger(out, stream, fill, digits, static_cast(count)); +} + +const GuestLocale::NumPutVtable g_numPutVtable{{DestroyCollate, DeleteCollate, RetainFacet, ReleaseFacet}, NumPutBool, NumPutLong, NumPutUnsignedLong, NumPutLongLong, NumPutUnsignedLongLong, NumPutDouble, NumPutLongDouble, NumPutPointer}; const GuestLocale::TimePutVtable g_timePutVtable{{DestroyCollate, DeleteCollate, RetainFacet, ReleaseFacet}, TimePutDoPut}; constexpr std::size_t GuestTimePutFacetSize = 0xb8; diff --git a/core/libs/tests/GuestLocale.cpp b/core/libs/tests/GuestLocale.cpp index ff349b7e20..52ea609ddd 100644 --- a/core/libs/tests/GuestLocale.cpp +++ b/core/libs/tests/GuestLocale.cpp @@ -10,11 +10,13 @@ #include #include #include +#include #include #include extern "C" { extern GuestLocale::Implementation* _ZSt21_sceLibcClassicLocale_nid_postfix; +extern std::uint64_t _ZNSt8numpunctIcE2idE_nid_postfix; extern std::uint64_t _ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix; extern std::uint64_t _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix; @@ -349,6 +351,74 @@ static void CheckTimePut() { facet->vtable->deleteObject(facet); } +GuestLocale::String* AssignShortString(GuestLocale::String* result, const char* text) { + *result = {}; + result->size = std::strlen(text); + result->capacity = 15; + std::memcpy(result->buffer, text, result->size); + return result; +} + +void APS5_VABI TestFacetNop(GuestLocale::Facet*) {} +GuestLocale::Facet* APS5_VABI TestFacetRelease(GuestLocale::Facet*) { return nullptr; } +char APS5_VABI TestDecimalPoint(const GuestLocale::NumpunctFacet*) { return ','; } +char APS5_VABI TestThousandsSep(const GuestLocale::NumpunctFacet*) { return '.'; } +GuestLocale::String* APS5_VABI TestGrouping(GuestLocale::String* result, const GuestLocale::NumpunctFacet*) { return AssignShortString(result, "\1\2"); } +GuestLocale::String* APS5_VABI TestFalsename(GuestLocale::String* result, const GuestLocale::NumpunctFacet*) { return AssignShortString(result, "no"); } +GuestLocale::String* APS5_VABI TestTruename(GuestLocale::String* result, const GuestLocale::NumpunctFacet*) { return AssignShortString(result, "yes"); } +const GuestLocale::NumpunctVtable g_testNumpunctVtable{{TestFacetNop, TestFacetNop, TestFacetNop, TestFacetRelease}, TestDecimalPoint, TestThousandsSep, TestGrouping, TestFalsename, TestTruename}; + +template +static std::string NumPut(TPut put, GuestLocale::Implementation** locale, std::int32_t flags, std::int32_t precision, std::int32_t width) { + char storage[64]; + char* next = storage; + char* end = storage + sizeof(storage); + TestStreambuf stream{{&g_testStreambufVtable, {}, &next, {}, &end}, {}, 0, 0}; + GuestLocale::IosBase ios{}; + ios.flags = flags; + ios.precision = precision; + ios.width = width; + ios.locale = locale; + const GuestLocale::OutputIterator out = put(GuestLocale::OutputIterator{false, &stream.buffer}, &ios); + Require(!out.failed && out.buffer == &stream.buffer && ios.width == 0 && stream.offered == 0); + return std::string(storage, next); +} + +static void CheckNumPut() { + GuestLocale::Implementation** classic = &_ZSt21_sceLibcClassicLocale_nid_postfix; + const GuestLocale::Implementation* locale = _ZSt21_sceLibcClassicLocale_nid_postfix; + GuestLocale::Facet* facet = nullptr; + Require(_ZNSt7num_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE7_GetcatEPPKNSt6locale5facetEPKS5__nid_postfix(&facet, &locale) == 4); + const auto* put = reinterpret_cast(facet->vtable); + const double infinity = std::numeric_limits::infinity(); + Require(NumPut([&](auto out, auto* ios) { return put->putLong(facet, out, ios, '*', -42); }, classic, 0x408, 6, 8) == "-*****42"); + Require(NumPut([&](auto out, auto* ios) { return put->putLong(facet, out, ios, '*', 255); }, classic, 0x2c8, 6, 10) == "0X******FF"); + Require(NumPut([&](auto out, auto* ios) { return put->putUnsignedLong(facet, out, ios, '*', std::numeric_limits::max()); }, classic, 0x20, 6, 0) == "1777777777777777777777"); + Require(NumPut([&](auto out, auto* ios) { return put->putLongLong(facet, out, ios, '*', std::numeric_limits::min()); }, classic, 0x2, 6, 25) == "-9223372036854775808*****"); + Require(NumPut([&](auto out, auto* ios) { return put->putBool(facet, out, ios, '*', true); }, classic, 0x8002, 6, 7) == "true***"); + Require(NumPut([&](auto out, auto* ios) { return put->putBool(facet, out, ios, '*', false); }, classic, 0, 6, 0) == "0"); + Require(NumPut([&](auto out, auto* ios) { return put->putPointer(facet, out, ios, '*', reinterpret_cast(0x1234)); }, classic, 0, 6, 6) == "**1234"); + Require(NumPut([&](auto out, auto* ios) { return put->putDouble(facet, out, ios, '*', 1234567.891); }, classic, 0, 6, 0) == "1.23457e+06"); + Require(NumPut([&](auto out, auto* ios) { return put->putDouble(facet, out, ios, '*', 1234567.891); }, classic, 0x1000, 2, 0) == "1234567.89"); + Require(NumPut([&](auto out, auto* ios) { return put->putDouble(facet, out, ios, '*', -0.000123); }, classic, 0xa00, 3, 12) == "**-1.230E-04"); + Require(NumPut([&](auto out, auto* ios) { return put->putDouble(facet, out, ios, '*', -1.5); }, classic, 0x1808, 6, 12) == "-***0x1.8p+0"); + Require(NumPut([&](auto out, auto* ios) { return put->putDouble(facet, out, ios, '*', 12345678901234.0); }, classic, 0x1000, 0, 0) == "12345678901234"); + Require(NumPut([&](auto out, auto* ios) { return put->putDouble(facet, out, ios, '*', -infinity); }, classic, 0x400, 6, 0) == "-inf"); + Require(NumPut([&](auto out, auto* ios) { return put->putLongDouble(facet, out, ios, '*', -2.25L); }, classic, 0, 3, 0) == "-2.25"); + RequireException([&] { NumPut([&](auto out, auto* ios) { return put->putDouble(facet, out, ios, '*', 1.0); }, classic, 0x1000, -45, 0); }); + GuestLocale::Facet punct{&g_testNumpunctVtable.facet, 1, 0}; + std::vector facets(_ZNSt8numpunctIcE2idE_nid_postfix + 1); + facets.back() = &punct; + GuestLocale::Implementation custom{{nullptr, 1, 0}, facets.data(), facets.size(), 0, false, "C"}; + GuestLocale::Implementation* customPointer = &custom; + Require(NumPut([&](auto out, auto* ios) { return put->putLong(facet, out, ios, '*', 1234567); }, &customPointer, 0, 6, 0) == "12.34.56.7"); + Require(NumPut([&](auto out, auto* ios) { return put->putDouble(facet, out, ios, '*', 1234567.5); }, &customPointer, 0x1000, 1, 0) == "12.34.56.7,5"); + Require(NumPut([&](auto out, auto* ios) { return put->putDouble(facet, out, ios, '*', infinity); }, &customPointer, 0x1000, 1, 0) == "inf"); + Require(NumPut([&](auto out, auto* ios) { return put->putDouble(facet, out, ios, '*', -infinity); }, &customPointer, 0, 6, 0) == "-inf"); + Require(NumPut([&](auto out, auto* ios) { return put->putBool(facet, out, ios, '*', true); }, &customPointer, 0x8000, 6, 5) == "**yes"); + facet->vtable->deleteObject(facet); +} + static void CheckFacetGetcat() { const GuestLocale::Implementation* locale = _ZSt21_sceLibcClassicLocale_nid_postfix; GuestLocale::Facet* codecvt = nullptr; @@ -421,6 +491,7 @@ int main() { _Unlocksyslock_nid_postfix(); _Unlocksyslock_nid_postfix(); CheckTimePut(); + CheckNumPut(); std::cout << "Guest locale checks passed\n"; } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 66af59cf94..18a696d4aa 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -140,7 +140,8 @@ - [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 go to glibc, which prints NaN the same way. The `_s` forms throw for an unknown conversion, `%m` included, which was not measured - [%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. Unlike `collate`, they ignore the locale argument and always build the C-locale facet; the console's behaviour for a non-C name is unverified. `num_put` formatting virtuals throw rather than fabricate output. `time_put`'s `do_put` formats through this library's `strftime` instead of the facet's unpopulated `_Locinfo` table, so the `strftime` gaps apply. `codecvt` reproduces the console virtuals (`noconv` conversions, `length` the shorter range); `numpunct` copies its C-locale table (`""`, `.`, `,`, `false`, `true`). The facets share the no-op `destroy`/`deleteObject`/`retain`/`release` base used for `collate`, so `numpunct`'s static C-locale strings are never freed. +- [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. Unlike `collate`, they ignore the locale argument and always build the C-locale facet; the console's behaviour for a non-C name is unverified. `time_put`'s `do_put` formats through this library's `strftime` instead of the facet's unpopulated `_Locinfo` table, so the `strftime` gaps apply. `codecvt` reproduces the console virtuals (`noconv` conversions, `length` the shorter range); `numpunct` copies its C-locale table (`""`, `.`, `,`, `false`, `true`). The facets share the no-op `destroy`/`deleteObject`/`retain`/`release` base used for `collate`, so `numpunct`'s static C-locale strings are never freed. +- [num_put::do_put](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) follows the SDK 7.00 `libc.prx` that PPSA06416 ships, read from its disassembly and run, but formats through this library's `snprintf`. That converts floating point exactly and rounds ties to even, where the module keeps about 18 exact significant digits, prints later ones from an inexact scaling (1e10 at precision 22 is `9999999999.999999999999`) and gives `-3` for -2.5 at precision 0. A precision so negative that the module's buffer of precision + 50 bytes cannot hold the result throws, where the module reads stale bytes. A stream locale without a `ctype` facet leaves digits unwidened, as the console's C-locale facet does. - [std::clog, std::wclog and the facet ids](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc): `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. - [std::__throw_bad_variant_access](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libc) throws `std::bad_variant_access` with the `what()` string `bad_variant_access`, the one [LLVM libc++](https://github.com/llvm/llvm-project/blob/main/libcxx/src/variant.cpp) returns, since the export carries libc++'s `__throw_bad_variant_access` name; the console's string is unverified. Onimusha: Way of the Sword (PPSA27836 01.000.001) imports it from `libc`, and its bundled `libc.prx` does not export it. - [std::bad_optional_access and std::_Throw_bad_optional_access](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libc) provide the exception type, constructor, destructor, and `what()` exports alongside `_Throw_bad_optional_access`, returning the `what()` string `bad_optional_access` matching Dinkumware and libc++ conventions; the console libc.prx string is unverified. PPSA26578 (issue #7624) and PPSA34015 import `_ZSt26_Throw_bad_optional_accessv` (`KwzEip7m6TM`) from `libc`.