diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 2cb3007787..670c6d3531 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -23,6 +23,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_stream_purge_tests ${LIBS_INCLUDE_DIR}/tests/GuestStreamPurge.cpp) + target_include_directories(guest_stream_purge_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_stream_purge_tests PRIVATE libc) + configure_windows_unwind(guest_stream_purge_tests) + add_test(NAME guest_stream_purge COMMAND guest_stream_purge_tests) + set_tests_properties(guest_stream_purge PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(guest_stream_purge PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_safe_error_tests ${LIBS_INCLUDE_DIR}/tests/GuestSafeErrors.cpp) target_include_directories(guest_safe_error_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_safe_error_tests PRIVATE libc) @@ -71,6 +81,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/FileLock.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/StdioExtras.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/StandardStreams.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/StreamPurge.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/GuestAllocations.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/GuestArena.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/GuestStacks.cpp diff --git a/core/libs/prx/libc/include/Api.hpp b/core/libs/prx/libc/include/Api.hpp index a45fb9037d..187bd4eb87 100644 --- a/core/libs/prx/libc/include/Api.hpp +++ b/core/libs/prx/libc/include/Api.hpp @@ -235,6 +235,7 @@ int APS5_VABI fclose_nid_postfix(FileStream* stream); FileStream* APS5_VABI _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(const char* filename, int mode, int protection); size_t APS5_VABI fread_nid_postfix(void* buffer, size_t size, size_t count, FileStream* stream); size_t APS5_VABI fwrite_nid_postfix(const void* buffer, size_t size, size_t count, FileStream* stream); +int APS5_VABI fpurge_nid_postfix(FileStream*); int APS5_VABI fseeko_nid_postfix(FileStream* stream, std::int64_t offset, int origin); std::int64_t APS5_VABI ftello_nid_postfix(FileStream* stream); int APS5_VABI fseek_nid_postfix(FileStream* stream, std::int64_t offset, int origin); diff --git a/core/libs/prx/libc/src/StreamPurge.cpp b/core/libs/prx/libc/src/StreamPurge.cpp new file mode 100644 index 0000000000..eff04b3442 --- /dev/null +++ b/core/libs/prx/libc/src/StreamPurge.cpp @@ -0,0 +1,38 @@ +#include "prx/libc/include/Api.hpp" +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#if defined(__linux__) +#include +#endif + +namespace { + +class LockedStream { + FileStream* stream; + +public: + explicit LockedStream(FileStream* value) : stream(value) { flockfile_nid_postfix(stream); } + ~LockedStream() { funlockfile_nid_postfix(stream); } +}; + +} + +extern "C" int APS5_VABI fpurge_nid_postfix(FileStream* stream) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } + auto* native = GetStreamHandle(stream); + const LockedStream lock(stream); +#ifdef _WIN32 + auto* state = reinterpret_cast(native); + state->pointer = state->base; + state->count = 0; +#elif defined(__linux__) + ::__fpurge(native); +#else + if (::fpurge(native) != 0) return -1; +#endif + stream->GuestState().position = stream->GuestState().buffer; + stream->SyncUnlockedStatus(); + return 0; +} diff --git a/core/libs/tests/GuestStreamPurge.cpp b/core/libs/tests/GuestStreamPurge.cpp new file mode 100644 index 0000000000..0303fe2a34 --- /dev/null +++ b/core/libs/tests/GuestStreamPurge.cpp @@ -0,0 +1,128 @@ +#include "prx/libc/include/Api.hpp" +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#include +#else +#include +#endif + +namespace { + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "stream purge: %s\n", message); + std::abort(); + } +} + +std::int64_t Position(std::FILE* stream) { +#ifdef _WIN32 + return ::_telli64(::_fileno(stream)); +#else + return ::lseek(::fileno(stream), 0, SEEK_CUR); +#endif +} + +struct BufferedStream { + std::array buffer{}; + std::FILE* native = std::tmpfile(); + FileStream stream{native}; + + BufferedStream() { Require(std::setvbuf(native, buffer.data(), _IOFBF, buffer.size()) == 0, "set full buffering"); } + ~BufferedStream() { if (stream.IsOpen()) stream.Close(); } +}; + +void CheckOutput() { + BufferedStream file; + Require(fwrite_nid_postfix("discard", 1, 7, &file.stream) == 7, "buffer output"); + Require(Position(file.native) == 0, "output is pending in the stream buffer"); + errno = 123; + Require(fpurge_nid_postfix(&file.stream) == 0 && errno == 123, "purge preserves errno on success"); + Require(Position(file.native) == 0, "purge does not write or seek"); + Require(fpurge_nid_postfix(&file.stream) == 0, "purge an empty buffer twice"); + Require(fwrite_nid_postfix("kept", 1, 4, &file.stream) == 4, "write after purging"); + Require(std::fflush(file.native) == 0 && Position(file.native) == 4, "flush only subsequent output"); + Require(std::fseek(file.native, 0, SEEK_SET) == 0, "rewind subsequent output"); + char bytes[8]{}; + Require(std::fread(bytes, 1, sizeof(bytes), file.native) == 4 && std::memcmp(bytes, "kept", 4) == 0, "discarded output never reaches the file"); +} + +void CheckInput() { + BufferedStream file; + Require(std::fwrite("abcdef", 1, 6, file.native) == 6 && std::fflush(file.native) == 0, "prepare input"); + Require(std::fseek(file.native, 0, SEEK_SET) == 0, "rewind input"); + Require(fgetc_nid_postfix(&file.stream) == 'a', "read one buffered byte"); + const auto position = Position(file.native); + Require(position > 1, "input has read-ahead data"); + Require(ungetc_nid_postfix('X', &file.stream) == 'X', "push back a different byte"); + Require(fpurge_nid_postfix(&file.stream) == 0, "purge input and pushback"); + Require(Position(file.native) == position, "purging input does not reposition the descriptor"); + Require(fgetc_nid_postfix(&file.stream) == EOF, "read-ahead and pushback have been discarded"); + Require((file.stream.GuestState().flags & 0x20) != 0, "publish EOF"); + Require(fpurge_nid_postfix(&file.stream) == 0 && (file.stream.GuestState().flags & 0x20) != 0, "purge preserves EOF"); + file.stream.SetError(); + Require(fpurge_nid_postfix(&file.stream) == 0 && (file.stream.GuestState().flags & 0x60) == 0x60, "purge preserves EOF and error"); +} + +void CheckUnbuffered() { + BufferedStream file; + Require(std::setvbuf(file.native, nullptr, _IONBF, 0) == 0, "disable buffering"); + Require(std::fwrite("ab", 1, 2, file.native) == 2, "write unbuffered input"); + Require(std::fseek(file.native, 0, SEEK_SET) == 0, "rewind unbuffered input"); + Require(fgetc_nid_postfix(&file.stream) == 'a', "read unbuffered byte"); + const auto position = Position(file.native); + Require(position >= 1 && position <= 2, "unbuffered descriptor position"); + Require(ungetc_nid_postfix('X', &file.stream) == 'X', "push back on an unbuffered stream"); + Require(fpurge_nid_postfix(&file.stream) == 0 && Position(file.native) == position, "purge unbuffered pushback without seeking"); + Require(fgetc_nid_postfix(&file.stream) == (position == 1 ? 'b' : EOF), "resume at the actual descriptor position"); +} + +void CheckPipe() { + int descriptors[2]; +#ifdef _WIN32 + Require(::_pipe(descriptors, 4096, _O_BINARY) == 0, "create pipe"); + auto* native = ::_fdopen(descriptors[0], "rb"); + Require(::_write(descriptors[1], "abcdef", 6) == 6 && ::_close(descriptors[1]) == 0, "fill pipe and close writer"); +#else + Require(::pipe(descriptors) == 0, "create pipe"); + auto* native = ::fdopen(descriptors[0], "rb"); + Require(::write(descriptors[1], "abcdef", 6) == 6 && ::close(descriptors[1]) == 0, "fill pipe and close writer"); +#endif + Require(native != nullptr, "open pipe stream"); + std::array buffer{}; + Require(std::setvbuf(native, buffer.data(), _IOFBF, buffer.size()) == 0, "buffer pipe input"); + FileStream stream{native}; + Require(fgetc_nid_postfix(&stream) == 'a' && ungetc_nid_postfix('X', &stream) == 'X', "read and push back on a pipe"); + Require(fpurge_nid_postfix(&stream) == 0, "purge does not require a seekable descriptor"); + Require(fgetc_nid_postfix(&stream) == EOF, "discard pipe read-ahead and pushback"); + Require(stream.Close() == 0, "close pipe stream"); +} +void CheckInvalid() { + BufferedStream file; + Require(file.stream.Close() == 0, "close stream"); + errno = 0; + Require(fpurge_nid_postfix(&file.stream) == -1 && errno == 9, "closed stream reports EBADF"); + bool rejected = false; + try { fpurge_nid_postfix(nullptr); } + catch (const std::runtime_error&) { rejected = true; } + Require(rejected, "null stream throws as existing stream functions do"); +} + +} + +int main() { + CheckOutput(); + CheckInput(); + CheckUnbuffered(); + CheckPipe(); + CheckInvalid(); + std::puts("stream purge tests passed"); +} diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 1230757d84..17f23e03a5 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -71,6 +71,8 @@ - [snprintf and vsnprintf](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) accept any size up to `SIZE_MAX`, as a payload process on a PS5 (firmware 13.60) does. A null buffer with a nonzero size throws here, where the console writes through it, and a result longer than `INT_MAX` throws, which was not measured +- [fpurge](../../../core/libs/prx/libc/src/StreamPurge.cpp) follows FreeBSD 11.4 [fpurge.c](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/stdio/fpurge.c): discard buffered input, pushback and output without writing or seeking, preserving EOF and error status, and report `EBADF` for a closed stream. Console behavior is unverified. The Windows path resets the existing host UCRT buffer representation; Linux and macOS use their native purge operations. Null streams throw as existing stream wrappers do. + ### Functional - [_Cosh](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the two-argument Dinkumware CRT helper used by `std::complex`, including a scaled large-finite-input path; the signature, exact result and floating-point exception behavior are not verified against the console.