From 4f14a20b838c4973dd4a2d1f77c076f77d762ad0 Mon Sep 17 00:00:00 2001 From: cocoa55 <284489448+cocoa55@users.noreply.github.com> Date: Tue, 27 Oct 2026 05:11:42 +0000 Subject: [PATCH 1/3] feat(libc): read 16-bit wide fields in fscanf on Linux fscanf, vfscanf, scanf, vscanf and the _s stream forms threw on %ls, %lc, %l[, %S and %C on Linux. Route those formats through the guest scanner sscanf already uses, reading the stream through StreamSource. The wide field reader now works on any source through Peek and Skip instead of a contiguous buffer: it decides whether to take a character from its lead byte, then consumes the whole UTF-8 sequence, so a rejected character stays in the stream without multi-byte pushback. AI-assisted: yes --- core/libs/prx/libc/include/ScanWideField.hpp | 1 + core/libs/prx/libc/src/Formatting.cpp | 79 ++++++++++--------- core/libs/prx/libc/src/ScanWideField.cpp | 17 ++-- core/libs/tests/GuestWideStreamScan.cpp | 82 ++++++++++++++++++++ docs/dev/debt/libc.md | 4 +- 5 files changed, 135 insertions(+), 48 deletions(-) diff --git a/core/libs/prx/libc/include/ScanWideField.hpp b/core/libs/prx/libc/include/ScanWideField.hpp index f210ec5d5ebf..27f230e266a6 100644 --- a/core/libs/prx/libc/include/ScanWideField.hpp +++ b/core/libs/prx/libc/include/ScanWideField.hpp @@ -6,6 +6,7 @@ namespace LibcDetail { +std::size_t WideScanCharacterLength(unsigned char lead); std::size_t ReadWideScanCharacter(const char* text, char32_t* point); bool InWideScanset(const std::string& specifier, char32_t point); void RequireNoPositionalWideScan(const char* format); diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index 1e908eee9116..e9568ab6659a 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -80,10 +80,6 @@ bool ScansWideInProcess(const char* format) { return true; } -void RejectGuestWideScan(const char* format) { - if (HasGuestWideScan(format)) NotImplemented_nid_no_patch("fscanf 16-bit wide conversion on Linux"); -} - bool IsWideScan(const std::string& length, char conversion) { return conversion == 'S' || conversion == 'C' || (length == "l" && (conversion == 's' || conversion == 'c' || conversion == '[')); } @@ -142,15 +138,12 @@ class StringSource { const char* position; public: - static constexpr bool Contiguous = true; - explicit StringSource(const char* text) : start(text), position(text) {} int Peek() const { return *position == '\0' ? EOF : static_cast(*position); } void Skip() { ++position; } void Advance(std::ptrdiff_t count) { position += count; } std::ptrdiff_t Consumed() const { return position - start; } - const char* Remaining() const { return position; } int Scan(const char* directive, bool suppress, void* target, int* consumed) const { return suppress ? HostSscanf(position, directive, consumed) : HostSscanf(position, directive, target, consumed); @@ -162,8 +155,6 @@ class StreamSource { std::ptrdiff_t consumed = 0; public: - static constexpr bool Contiguous = false; - explicit StreamSource(std::FILE* native) : file(native) {} int Peek() const { @@ -185,6 +176,21 @@ class StreamSource { } }; +#ifndef _WIN32 +template +char16_t TakeWideCharacter(TSource& source) { + char bytes[5] = {}; + const std::size_t length = std::max(LibcDetail::WideScanCharacterLength(static_cast(source.Peek())), 1); + for (std::size_t index = 0; index < length && (index == 0 || (source.Peek() & 0xC0) == 0x80); ++index) { + bytes[index] = static_cast(source.Peek()); + source.Skip(); + } + char32_t point = 0; + LibcDetail::ReadWideScanCharacter(bytes, &point); + return static_cast(point); +} +#endif + template int ScanSource(TSource source, const char* format, bool secure, NextPointer nextPointer, NextCapacity nextCapacity) { if (format == nullptr) return EOF; @@ -242,14 +248,12 @@ int ScanSource(TSource source, const char* format, bool secure, NextPointer next continue; } #ifndef _WIN32 - if constexpr (TSource::Contiguous) if (IsWideScan(length, conversion)) { - const char* const input = source.Remaining(); + if (IsWideScan(length, conversion)) { const char kind = conversion == 'S' ? 's' : conversion == 'C' ? 'c' : conversion; - const char* field = input; if (kind == 's') { - while (std::isspace(static_cast(*field))) ++field; + while (std::isspace(source.Peek())) source.Skip(); } - if (*field == '\0') return finish(converted ? assigned : EOF); + if (source.Peek() == EOF) return finish(converted ? assigned : EOF); auto* target = suppress ? nullptr : static_cast(nextPointer()); unsigned long limit = ULONG_MAX; if (secure && !suppress) { @@ -263,33 +267,23 @@ int ScanSource(TSource source, const char* format, bool secure, NextPointer next const unsigned long wanted = width != 0 ? width : (kind == 'c' ? 1 : ULONG_MAX); const unsigned long taken = std::min(wanted, limit); std::u16string text; - const auto accepts = [&](char32_t point) { - if (kind == 's') return point >= 0x80 || !std::isspace(static_cast(point)); - if (kind == '[') return LibcDetail::InWideScanset(specifier, point); + const auto accepts = [&](int lead) { + if (lead == EOF) return false; + if (kind == 's') return lead >= 0x80 || !std::isspace(lead); + if (kind == '[') return LibcDetail::InWideScanset(specifier, static_cast(lead)); return true; }; - while (text.size() < taken && *field != '\0') { - char32_t point = 0; - const std::size_t size = LibcDetail::ReadWideScanCharacter(field, &point); - if (!accepts(point)) break; - text.push_back(static_cast(point)); - field += size; - } + while (text.size() < taken && accepts(source.Peek())) text.push_back(TakeWideCharacter(source)); if (text.empty()) return finish(assigned); - if (target != nullptr && kind != 'c' && wanted > limit && text.size() == limit && *field != '\0') { - char32_t point = 0; - LibcDetail::ReadWideScanCharacter(field, &point); - if (accepts(point)) { - target[0] = u'\0'; - return finish(assigned); - } + if (target != nullptr && kind != 'c' && wanted > limit && text.size() == limit && accepts(source.Peek())) { + target[0] = u'\0'; + return finish(assigned); } if (target != nullptr) { std::memcpy(target, text.data(), text.size() * sizeof(char16_t)); if (kind != 'c') target[text.size()] = u'\0'; ++assigned; } - source.Advance(field - input); converted = true; continue; } @@ -360,7 +354,7 @@ int ScanGuestStream(FileStream* stream, const char* format, NextPointer nextPoin auto* native = GetNativeStream(stream); if (!stream->SwitchToReading()) return EOF; #ifndef _WIN32 - RejectGuestWideScan(format); + if (HasGuestWideScan(format)) LibcDetail::RequireNoPositionalWideScan(format); #endif const int result = ScanSource(StreamSource(native), format, true, nextPointer, nextCapacity); stream->SyncStatus(); @@ -458,10 +452,10 @@ int APS5_VABI fscanf_nid_postfix(FileStream* stream, const char* format, ...) { const int result = LibcDetail::ScanFileWindows_nid_no_patch(native, format, args); __builtin_sysv_va_end(args); #else - RejectGuestWideScan(format); std::va_list args; va_start(args, format); - const int result = HostVfscanf(native, format, args); + const int result = ScansWideInProcess(format) ? ScanSource(StreamSource(native), format, false, [&] { return va_arg(args, void*); }, [] { return 0u; }) + : HostVfscanf(native, format, args); va_end(args); #endif stream->SyncStatus(); @@ -781,11 +775,16 @@ int APS5_VABI vfscanf_nid_postfix(FileStream* stream, const char* format, VaList #ifdef _WIN32 const int result = LibcDetail::ScanFileWindows_nid_no_patch(native, format, args); #else - RejectGuestWideScan(format); - std::va_list copy; - va_copy(copy, *reinterpret_cast(args)); - const int result = HostVfscanf(native, format, copy); - va_end(copy); + int result = 0; + if (ScansWideInProcess(format)) { + LibcDetail::FormatArguments arguments(args); + result = ScanSource(StreamSource(native), format, false, [&] { return arguments.Next(); }, [] { return 0u; }); + } else { + std::va_list copy; + va_copy(copy, *reinterpret_cast(args)); + result = HostVfscanf(native, format, copy); + va_end(copy); + } #endif stream->SyncStatus(); stream->KeepDirectoryReadError(result == -1); diff --git a/core/libs/prx/libc/src/ScanWideField.cpp b/core/libs/prx/libc/src/ScanWideField.cpp index 5d5a35559de0..616af9af1988 100644 --- a/core/libs/prx/libc/src/ScanWideField.cpp +++ b/core/libs/prx/libc/src/ScanWideField.cpp @@ -11,12 +11,9 @@ std::size_t DecodeUtf8(const char* text, char32_t* point) { *point = lead; return 1; } - std::size_t size = 0; - char32_t value = 0; - if ((lead & 0xE0) == 0xC0) { size = 2; value = lead & 0x1F; } - else if ((lead & 0xF0) == 0xE0) { size = 3; value = lead & 0x0F; } - else if ((lead & 0xF8) == 0xF0) { size = 4; value = lead & 0x07; } - else return 0; + const std::size_t size = LibcDetail::WideScanCharacterLength(lead); + if (size == 0) return 0; + char32_t value = lead & (0x7F >> size); for (std::size_t index = 1; index < size; ++index) { const auto next = static_cast(text[index]); if ((next & 0xC0) != 0x80) return 0; @@ -32,6 +29,14 @@ std::size_t DecodeUtf8(const char* text, char32_t* point) { namespace LibcDetail { +std::size_t WideScanCharacterLength(unsigned char lead) { + if (lead < 0x80) return 1; + if ((lead & 0xE0) == 0xC0) return 2; + if ((lead & 0xF0) == 0xE0) return 3; + if ((lead & 0xF8) == 0xF0) return 4; + return 0; +} + std::size_t ReadWideScanCharacter(const char* text, char32_t* point) { const std::size_t size = DecodeUtf8(text, point); if (size == 0) NotImplemented_nid_no_patch("scanf 16-bit wide conversion of input that is not UTF-8"); diff --git a/core/libs/tests/GuestWideStreamScan.cpp b/core/libs/tests/GuestWideStreamScan.cpp index 857a72001f45..6a37ad2965b7 100644 --- a/core/libs/tests/GuestWideStreamScan.cpp +++ b/core/libs/tests/GuestWideStreamScan.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include extern "C" { @@ -54,6 +55,29 @@ static int APS5_VABI ScanInputSized(const char16_t* format, ...) { return result; } +#ifndef _WIN32 +static int APS5_VABI ScanNarrow(FileStream* stream, const char* format, ...) { + APS5_VA_BEGIN(format); + const int result = vfscanf_nid_postfix(stream, format, reinterpret_cast(args)); + APS5_VA_END(); + return result; +} + +static int APS5_VABI ScanNarrowSized(FileStream* stream, const char* format, ...) { + APS5_VA_BEGIN(format); + const int result = vfscanf_s_nid_postfix(stream, format, reinterpret_cast(args)); + APS5_VA_END(); + return result; +} + +static int APS5_VABI ScanNarrowInput(const char* format, ...) { + APS5_VA_BEGIN(format); + const int result = vscanf_nid_postfix(format, reinterpret_cast(args)); + APS5_VA_END(); + return result; +} +#endif + static FileStream* Open(const char* text) { auto* stream = new FileStream(std::tmpfile()); Require(std::fputs(text, stream->GetHandle()) >= 0, "seed the stream"); @@ -161,9 +185,67 @@ static void CheckStandardInput() { Close(stream); } +#ifndef _WIN32 +static void CheckNarrowFormatWideFields() { + std::int32_t value = 0; + std::int64_t count = -1; + char16_t word[8]{}; + char16_t letter = u'!'; + char narrow[8]{}; + + FileStream* stream = Open(" \xc5\xbelu\xc5\xa5 k\xc5\xaf\xc5\x88 7"); + Require(fscanf_nid_postfix(stream, "%ls %lc", word, &letter) == 2 && std::u16string(word) == u"\u017elu\u0165" && letter == u'k', "fscanf %ls and %lc decode UTF-8"); + Require(ScanNarrow(stream, "%S%ln %d", word, &count, &value) == 2 && std::u16string(word) == u"\u016f\u0148" && count == 4 && value == 7, "vfscanf %S with %ln counting bytes"); + Require(fgetc_nid_postfix(stream) == EOF, "the whole input is consumed"); + Close(stream); + + stream = Open("12 ab cd"); + Require(fscanf_nid_postfix(stream, "%d %ls %7s", &value, word, narrow) == 3 && value == 12 && std::u16string(word) == u"ab" && std::strcmp(narrow, "cd") == 0, "wide and narrow fields share the stream"); + Close(stream); + + stream = Open("ab\xc3\xa9t rest"); + Require(fscanf_nid_postfix(stream, "%l[a-c]", word) == 1 && std::u16string(word) == u"ab", "a scanset stops at a non-ASCII character"); + Require(fscanf_nid_postfix(stream, "%l[^ ] %7s", word, narrow) == 2 && std::u16string(word) == u"\u00e9t" && std::strcmp(narrow, "rest") == 0, "the non-ASCII character stays in the stream"); + Close(stream); + + char16_t three[5] = {u'!', u'!', u'!', u'!', u'!'}; + stream = Open("xyzw"); + Require(fscanf_nid_postfix(stream, "%3lc", three) == 1 && three[0] == u'x' && three[1] == u'y' && three[2] == u'z' && three[3] == u'!', "%3lc stores three units without a terminator"); + Require(fgetc_nid_postfix(stream) == 'w', "the character after %3lc stays in the stream"); + Close(stream); + + stream = Open("xyz"); + Require(fscanf_nid_postfix(stream, "%l[a-c]", word) == 0, "a scanset matching nothing"); + Require(fgetc_nid_postfix(stream) == 'x', "the rejected character stays in the stream"); + Close(stream); + + stream = Open(" "); + Require(fscanf_nid_postfix(stream, "%ls", word) == EOF, "only white space before %ls"); + Close(stream); + + stream = Open("hello world"); + char16_t fits[6]{}; + char16_t small[4] = {u'!', u'!', u'!', u'!'}; + Require(fscanf_s_nid_postfix(stream, "%ls", fits, 6u) == 1 && std::u16string(fits) == u"hello", "fscanf_s %ls that fits"); + Require(ScanNarrowSized(stream, "%ls", small, 4u) == 0 && small[0] == 0, "vfscanf_s rejects a too small buffer"); + Close(stream); + + FileStream* const standardInput = __stdinp_nid_postfix; + stream = Open("ab c"); + __stdinp_nid_postfix = stream; + Require(scanf_nid_postfix("%ls", word) == 1 && std::u16string(word) == u"ab", "scanf %ls reads the guest standard input"); + Require(ScanNarrowInput(" %C", &letter) == 1 && letter == u'c', "vscanf %C reads the guest standard input"); + __stdinp_nid_postfix = standardInput; + Close(stream); +} +#endif + int main() { CheckConsumption(); CheckWideText(); CheckStandardInput(); +#ifndef _WIN32 + CheckNarrowFormatWideFields(); +#endif return 0; } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index a5f6968e1677..534d080bb729 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -163,7 +163,7 @@ - [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`. -- [The scanf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc), the `_s` forms included, decodes the input of the 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`) on Linux as UTF-8, mirroring the narrow printf family, where FreeBSD uses the locale's `mbrtowc` (one byte per character in the C locale); the console's locale encoding is unverified. Input that is not UTF-8, a character outside the BMP, a scanset member outside ASCII, positional arguments next to a wide conversion and wide conversions in `fscanf` throw. Each character is stored as one 16-bit unit, widths and `_s` sizes count characters, and a `%lc` field cut short by the end of the input keeps what it read, as FreeBSD's `convert_wchar` does. Windows hands `%ls`, `%lc` and `%l[` to the host, whose `wchar_t` is 16 bits +- [The scanf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc), the `_s` forms included, decodes the input of the 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`) on Linux as UTF-8, mirroring the narrow printf family, where FreeBSD uses the locale's `mbrtowc` (one byte per character in the C locale); the console's locale encoding is unverified. Input that is not UTF-8, a character outside the BMP, a scanset member outside ASCII and positional arguments next to a wide conversion throw. On a stream a field stops before the first character it does not take, which stays in the stream. Each character is stored as one 16-bit unit, widths and `_s` sizes count characters, and a `%lc` field cut short by the end of the input keeps what it read, as FreeBSD's `convert_wchar` does. Windows hands `%ls`, `%lc` and `%l[` to the host, whose `wchar_t` is 16 bits - [The scanf family](../../../core/libs/prx/libc/include/WindowsScanning.hpp) (libc) on Windows returns 0, as C17 and FreeBSD do, when input fails after only suppressed conversions have completed (`sscanf("12", "%*d%d", &value)`). Linux hands the format to glibc, which returns `EOF` there. On Windows the host stops a `%x`, `%X`, `%i` or `%p` after the `x` of a `0x` that no hex digit follows (or that the width cuts off), and a float conversion (`%e`, `%f`, `%g`, `%a`) after the `e` of an exponent with no digits (`1e`), where C17 and FreeBSD stop before the `x` or the `e` - [The scanf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) on Linux reads a `q` length as `ll`, as glibc does. On Windows a `q` directive ends the scan with nothing stored, as the console's own libc does for `%qd`; FreeBSD 11.4's source reads it as `ll`. - [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.. A relinked module's `.guest.prx` is matched the same way when the original module is not beside it: Borderlands 4 (PPSA01494) loads `/app0/prx/libRenoirCore.PS5.prx`, which it ships as `librenoircore.ps5.prx`. A relinked module request that misses both the exact and the case-insensitive `.guest.prx` takes the sibling whose stem (the name without its last extension, ASCII case ignored) equals the requested stem and that ends with `.guest.prx`, shortest name first: Unity IL2CPP titles load `Media/Modules/.prx` and `Media/Plugins/.prx`, and the relinker names the converted module after the ELF it was built from, `.elf.guest.prx`. The console's module namespace is assumed to ignore case and the extension; several stem matches pick the shortest name instead of throwing, which is not measured on a console @@ -200,5 +200,5 @@ - [printf family conversions](../../../core/libs/prx/libc/include/WindowsFormatting.hpp) (libc) - Windows `FormatWindows` follows the title `libc.prx` `_Printf` measured from PPSA13934, PPSA07923, PPSA05916 and PPSA04158: `%q`, `%D`, `%O` and `%U` are unknown conversions; an unknown character is printed with its leading `%`, and width, `-` and discarded length modifiers behave as measured (`%5y` is ` %y`, `%hhz` is `%z`). `%%` takes a width. A trailing `%` reads past the format's NUL and is not reproduced; it throws. `#`, precision and a `0` without a width are ignored, as a payload process on a PS5 (firmware 13.60) printed `%#k` and `%.2k` as `%k`; `+`, space, `'` and `0` with a width on an unknown conversion are unverified and throw. On Linux a format with an unknown conversion also goes to this formatter - [child process registry](../../../core/libs/prx/libc/src/Process.cpp) (libc) - `RegisterChildProcess_nid_no_patch` and `WaitChildProcess_nid_no_patch` track the native handles of children created by `posix_spawn` ([libSceLibcInternal](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp)) so libkernel can reap them on Windows, where the host has no `waitpid`. The registry lives in libc because libSceLibcInternal imports libc but not libkernel. `RegisterChildProcess_nid_no_patch` stores the process ID and handle; only reaped handles are closed, and a process that exits without waiting leaks them. Registration without a live child is not validated, and a duplicate process ID is accepted. The decoder maps recognized structured-exception codes to guest signals, keeping the guest `exit` byte layout for anything else. These exports are internal glue for libkernel, not console ABI: their behavior is unverified against any PS5 library and a title importing their NIDs directly is not expected. - [fprintf_s, vfprintf_s, vprintf_s, fwprintf_s, wprintf_s, vfwprintf_s and vwprintf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) follow C11 K.3.5.3.1, K.3.5.3.10 and K.3.9.1.1 with the constraints of `snprintf_s` and `swprintf_s`: a null stream, format or argument list, any `%n`, and a null `%s`, `%ls` or `%S` argument return -1 and write nothing, because the output is formatted completely before it is written. A violation only returns: the constraint handler is not invoked, `errno` is untouched, and the value is the one the narrow `snprintf_s` returns on this branch, not a result measured for a stream function on a PS5. A closed stream sets `EBADF` (9) as `fprintf` does. `vprintf_s` and `vwprintf_s` write to the guest standard output stream, and the wide forms count characters while the narrow ones count bytes, as their unchecked siblings do -- [scanf_s, fscanf_s, vscanf_s and vfscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) run the scanner of `sscanf_s` over the stream, taking the size of `%s`, `%c` and `%[` as `unsigned int` after the pointer, so everything listed for `sscanf_s` applies, including the count of items converted before an undersized destination, whose first element is set to zero. Each directive is read from the stream with the host `fscanf`, with one character of look-ahead pushed back, so the stream stops after the last character a conversion used. A null stream, null format or null destination returns `EOF`, a closed stream also sets `EBADF`, and the constraint handler is not invoked. The 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`) throw on Linux, as in `fscanf`. None of the stream forms were measured on a PS5, and the size type follows `sscanf_s` rather than `rsize_t` +- [scanf_s, fscanf_s, vscanf_s and vfscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) run the scanner of `sscanf_s` over the stream, taking the size of `%s`, `%c` and `%[` as `unsigned int` after the pointer, so everything listed for `sscanf_s` applies, including the count of items converted before an undersized destination, whose first element is set to zero. Each directive is read from the stream with the host `fscanf`, with one character of look-ahead pushed back, so the stream stops after the last character a conversion used. A null stream, null format or null destination returns `EOF`, a closed stream also sets `EBADF`, and the constraint handler is not invoked. On Linux the 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`) are read by the same scanner as in `fscanf`. None of the stream forms were measured on a PS5, and the size type follows `sscanf_s` rather than `rsize_t` - [freopen_s](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) follows C11 K.3.5.2.2 through `freopen`: a null result, mode or stream returns `EINVAL` (22) with `*result` set to null when it exists, and a null name is passed to `freopen`, which reports mode-only reopening as unsupported (`ENOTSUP`, 45) with `*result` null, and the constraint handler is not invoked, as in `fopen_s`. A successful reopen stores the stream in `*result` and returns 0, and a failed one stores null and returns the errno `freopen` left, closing the stream as `freopen` does. The console's return values are unverified From 6a95925815d5db353ac80890fd008d04bf38df92 Mon Sep 17 00:00:00 2001 From: cocoa55 <284489448+cocoa55@users.noreply.github.com> Date: Tue, 27 Oct 2026 05:43:20 +0000 Subject: [PATCH 2/3] refactor(libc): decode the wide scan look-ahead before deciding Deciding from the lead byte only holds while scanset members are ASCII. Each source now decodes its next character with PeekWide without consuming it, and the field reader skips its bytes only once it takes the character. StreamSource peeks by reading the UTF-8 sequence and pushing it back with ungetc, as the fwscanf stream reader in ScanningWide.cpp already does. Keep TSource::Contiguous. AI-assisted: yes --- core/libs/prx/libc/src/Formatting.cpp | 69 ++++++++++----- core/libs/tests/GuestWideStreamScan.cpp | 110 ++++++++++++------------ 2 files changed, 101 insertions(+), 78 deletions(-) diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index e9568ab6659a..8c3b962bb1a5 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -138,6 +138,8 @@ class StringSource { const char* position; public: + static constexpr bool Contiguous = true; + explicit StringSource(const char* text) : start(text), position(text) {} int Peek() const { return *position == '\0' ? EOF : static_cast(*position); } @@ -145,6 +147,12 @@ class StringSource { void Advance(std::ptrdiff_t count) { position += count; } std::ptrdiff_t Consumed() const { return position - start; } +#ifndef _WIN32 + std::size_t PeekWide(char32_t* point) const { + return *position == '\0' ? 0 : LibcDetail::ReadWideScanCharacter(position, point); + } +#endif + int Scan(const char* directive, bool suppress, void* target, int* consumed) const { return suppress ? HostSscanf(position, directive, consumed) : HostSscanf(position, directive, target, consumed); } @@ -155,6 +163,8 @@ class StreamSource { std::ptrdiff_t consumed = 0; public: + static constexpr bool Contiguous = false; + explicit StreamSource(std::FILE* native) : file(native) {} int Peek() const { @@ -171,26 +181,31 @@ class StreamSource { void Advance(std::ptrdiff_t count) { consumed += count; } std::ptrdiff_t Consumed() const { return consumed; } +#ifndef _WIN32 + std::size_t PeekWide(char32_t* point) const { + char bytes[5] = {}; + int next = std::fgetc(file); + if (next == EOF) return 0; + const std::size_t length = std::max(LibcDetail::WideScanCharacterLength(static_cast(next)), 1); + std::size_t size = 0; + while (true) { + bytes[size++] = static_cast(next); + if (size == length || (next = std::fgetc(file)) == EOF) break; + if ((next & 0xC0) != 0x80) { + std::ungetc(next, file); + break; + } + } + for (std::size_t index = size; index != 0; --index) std::ungetc(static_cast(bytes[index - 1]), file); + return LibcDetail::ReadWideScanCharacter(bytes, point); + } +#endif + int Scan(const char* directive, bool suppress, void* target, int* count) const { return suppress ? HostFscanf(file, directive, count) : HostFscanf(file, directive, target, count); } }; -#ifndef _WIN32 -template -char16_t TakeWideCharacter(TSource& source) { - char bytes[5] = {}; - const std::size_t length = std::max(LibcDetail::WideScanCharacterLength(static_cast(source.Peek())), 1); - for (std::size_t index = 0; index < length && (index == 0 || (source.Peek() & 0xC0) == 0x80); ++index) { - bytes[index] = static_cast(source.Peek()); - source.Skip(); - } - char32_t point = 0; - LibcDetail::ReadWideScanCharacter(bytes, &point); - return static_cast(point); -} -#endif - template int ScanSource(TSource source, const char* format, bool secure, NextPointer nextPointer, NextCapacity nextCapacity) { if (format == nullptr) return EOF; @@ -267,17 +282,25 @@ int ScanSource(TSource source, const char* format, bool secure, NextPointer next const unsigned long wanted = width != 0 ? width : (kind == 'c' ? 1 : ULONG_MAX); const unsigned long taken = std::min(wanted, limit); std::u16string text; - const auto accepts = [&](int lead) { - if (lead == EOF) return false; - if (kind == 's') return lead >= 0x80 || !std::isspace(lead); - if (kind == '[') return LibcDetail::InWideScanset(specifier, static_cast(lead)); + const auto accepts = [&](char32_t point) { + if (kind == 's') return point >= 0x80 || !std::isspace(static_cast(point)); + if (kind == '[') return LibcDetail::InWideScanset(specifier, point); return true; }; - while (text.size() < taken && accepts(source.Peek())) text.push_back(TakeWideCharacter(source)); + while (text.size() < taken) { + char32_t point = 0; + const std::size_t size = source.PeekWide(&point); + if (size == 0 || !accepts(point)) break; + text.push_back(static_cast(point)); + for (std::size_t index = 0; index < size; ++index) source.Skip(); + } if (text.empty()) return finish(assigned); - if (target != nullptr && kind != 'c' && wanted > limit && text.size() == limit && accepts(source.Peek())) { - target[0] = u'\0'; - return finish(assigned); + if (target != nullptr && kind != 'c' && wanted > limit && text.size() == limit) { + char32_t point = 0; + if (source.PeekWide(&point) != 0 && accepts(point)) { + target[0] = u'\0'; + return finish(assigned); + } } if (target != nullptr) { std::memcpy(target, text.data(), text.size() * sizeof(char16_t)); diff --git a/core/libs/tests/GuestWideStreamScan.cpp b/core/libs/tests/GuestWideStreamScan.cpp index 6a37ad2965b7..1dd3c9ae2040 100644 --- a/core/libs/tests/GuestWideStreamScan.cpp +++ b/core/libs/tests/GuestWideStreamScan.cpp @@ -90,6 +90,61 @@ static void Close(FileStream* stream) { delete stream; } +#ifndef _WIN32 +static void CheckNarrowFormatWideFields() { + std::int32_t value = 0; + std::int64_t count = -1; + char16_t word[8]{}; + char16_t letter = u'!'; + char narrow[8]{}; + + FileStream* stream = Open(" \xc5\xbelu\xc5\xa5 k\xc5\xaf\xc5\x88 7"); + Require(fscanf_nid_postfix(stream, "%ls %lc", word, &letter) == 2 && std::u16string(word) == u"\u017elu\u0165" && letter == u'k', "fscanf %ls and %lc decode UTF-8"); + Require(ScanNarrow(stream, "%S%ln %d", word, &count, &value) == 2 && std::u16string(word) == u"\u016f\u0148" && count == 4 && value == 7, "vfscanf %S with %ln counting bytes"); + Require(fgetc_nid_postfix(stream) == EOF, "the whole input is consumed"); + Close(stream); + + stream = Open("12 ab cd"); + Require(fscanf_nid_postfix(stream, "%d %ls %7s", &value, word, narrow) == 3 && value == 12 && std::u16string(word) == u"ab" && std::strcmp(narrow, "cd") == 0, "wide and narrow fields share the stream"); + Close(stream); + + stream = Open("ab\xc3\xa9t rest"); + Require(fscanf_nid_postfix(stream, "%l[a-c]", word) == 1 && std::u16string(word) == u"ab", "a scanset stops at a non-ASCII character"); + Require(fscanf_nid_postfix(stream, "%l[^ ] %7s", word, narrow) == 2 && std::u16string(word) == u"\u00e9t" && std::strcmp(narrow, "rest") == 0, "the non-ASCII character stays in the stream"); + Close(stream); + + char16_t three[5] = {u'!', u'!', u'!', u'!', u'!'}; + stream = Open("xyzw"); + Require(fscanf_nid_postfix(stream, "%3lc", three) == 1 && three[0] == u'x' && three[1] == u'y' && three[2] == u'z' && three[3] == u'!', "%3lc stores three units without a terminator"); + Require(fgetc_nid_postfix(stream) == 'w', "the character after %3lc stays in the stream"); + Close(stream); + + stream = Open("xyz"); + Require(fscanf_nid_postfix(stream, "%l[a-c]", word) == 0, "a scanset matching nothing"); + Require(fgetc_nid_postfix(stream) == 'x', "the rejected character stays in the stream"); + Close(stream); + + stream = Open(" "); + Require(fscanf_nid_postfix(stream, "%ls", word) == EOF, "only white space before %ls"); + Close(stream); + + stream = Open("hello world"); + char16_t fits[6]{}; + char16_t small[4] = {u'!', u'!', u'!', u'!'}; + Require(fscanf_s_nid_postfix(stream, "%ls", fits, 6u) == 1 && std::u16string(fits) == u"hello", "fscanf_s %ls that fits"); + Require(ScanNarrowSized(stream, "%ls", small, 4u) == 0 && small[0] == 0, "vfscanf_s rejects a too small buffer"); + Close(stream); + + FileStream* const standardInput = __stdinp_nid_postfix; + stream = Open("ab c"); + __stdinp_nid_postfix = stream; + Require(scanf_nid_postfix("%ls", word) == 1 && std::u16string(word) == u"ab", "scanf %ls reads the guest standard input"); + Require(ScanNarrowInput(" %C", &letter) == 1 && letter == u'c', "vscanf %C reads the guest standard input"); + __stdinp_nid_postfix = standardInput; + Close(stream); +} +#endif + static void CheckConsumption() { std::int32_t value = -1; std::int64_t count = -1; @@ -185,61 +240,6 @@ static void CheckStandardInput() { Close(stream); } -#ifndef _WIN32 -static void CheckNarrowFormatWideFields() { - std::int32_t value = 0; - std::int64_t count = -1; - char16_t word[8]{}; - char16_t letter = u'!'; - char narrow[8]{}; - - FileStream* stream = Open(" \xc5\xbelu\xc5\xa5 k\xc5\xaf\xc5\x88 7"); - Require(fscanf_nid_postfix(stream, "%ls %lc", word, &letter) == 2 && std::u16string(word) == u"\u017elu\u0165" && letter == u'k', "fscanf %ls and %lc decode UTF-8"); - Require(ScanNarrow(stream, "%S%ln %d", word, &count, &value) == 2 && std::u16string(word) == u"\u016f\u0148" && count == 4 && value == 7, "vfscanf %S with %ln counting bytes"); - Require(fgetc_nid_postfix(stream) == EOF, "the whole input is consumed"); - Close(stream); - - stream = Open("12 ab cd"); - Require(fscanf_nid_postfix(stream, "%d %ls %7s", &value, word, narrow) == 3 && value == 12 && std::u16string(word) == u"ab" && std::strcmp(narrow, "cd") == 0, "wide and narrow fields share the stream"); - Close(stream); - - stream = Open("ab\xc3\xa9t rest"); - Require(fscanf_nid_postfix(stream, "%l[a-c]", word) == 1 && std::u16string(word) == u"ab", "a scanset stops at a non-ASCII character"); - Require(fscanf_nid_postfix(stream, "%l[^ ] %7s", word, narrow) == 2 && std::u16string(word) == u"\u00e9t" && std::strcmp(narrow, "rest") == 0, "the non-ASCII character stays in the stream"); - Close(stream); - - char16_t three[5] = {u'!', u'!', u'!', u'!', u'!'}; - stream = Open("xyzw"); - Require(fscanf_nid_postfix(stream, "%3lc", three) == 1 && three[0] == u'x' && three[1] == u'y' && three[2] == u'z' && three[3] == u'!', "%3lc stores three units without a terminator"); - Require(fgetc_nid_postfix(stream) == 'w', "the character after %3lc stays in the stream"); - Close(stream); - - stream = Open("xyz"); - Require(fscanf_nid_postfix(stream, "%l[a-c]", word) == 0, "a scanset matching nothing"); - Require(fgetc_nid_postfix(stream) == 'x', "the rejected character stays in the stream"); - Close(stream); - - stream = Open(" "); - Require(fscanf_nid_postfix(stream, "%ls", word) == EOF, "only white space before %ls"); - Close(stream); - - stream = Open("hello world"); - char16_t fits[6]{}; - char16_t small[4] = {u'!', u'!', u'!', u'!'}; - Require(fscanf_s_nid_postfix(stream, "%ls", fits, 6u) == 1 && std::u16string(fits) == u"hello", "fscanf_s %ls that fits"); - Require(ScanNarrowSized(stream, "%ls", small, 4u) == 0 && small[0] == 0, "vfscanf_s rejects a too small buffer"); - Close(stream); - - FileStream* const standardInput = __stdinp_nid_postfix; - stream = Open("ab c"); - __stdinp_nid_postfix = stream; - Require(scanf_nid_postfix("%ls", word) == 1 && std::u16string(word) == u"ab", "scanf %ls reads the guest standard input"); - Require(ScanNarrowInput(" %C", &letter) == 1 && letter == u'c', "vscanf %C reads the guest standard input"); - __stdinp_nid_postfix = standardInput; - Close(stream); -} -#endif - int main() { CheckConsumption(); CheckWideText(); From 6447ec7dd2c8f92cb4a6c4391cd3477f27a088e0 Mon Sep 17 00:00:00 2001 From: cocoa55 <284489448+cocoa55@users.noreply.github.com> Date: Thu, 29 Oct 2026 20:50:52 +0000 Subject: [PATCH 3/3] fix(libc): keep StringSource::Remaining for prefix handling #12530 reads source.Remaining() in the contiguous branch of ScanSource, so dropping the accessor here would break the build whichever of the two lands second, without a textual conflict. AI-assisted: yes --- core/libs/prx/libc/src/Formatting.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index 8c3b962bb1a5..16af8b52e941 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -146,6 +146,7 @@ class StringSource { void Skip() { ++position; } void Advance(std::ptrdiff_t count) { position += count; } std::ptrdiff_t Consumed() const { return position - start; } + const char* Remaining() const { return position; } #ifndef _WIN32 std::size_t PeekWide(char32_t* point) const {