Merge pull request #10139 from dotaku22/feat/libc-fpurge

feat(libc): implement buffered stream purge
This commit is contained in:
Dean authored and GitHub committed 2026-10-27 10:53:27 +13:00
commit 806a573b0b
5 files changed
+180

No files matched your search

+11
View File
@@ -23,6 +23,16 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
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:$<TARGET_FILE_DIR:libc>")
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
+1
View File
@@ -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);
+38
View File
@@ -0,0 +1,38 @@
#include "prx/libc/include/Api.hpp"
#include "prx/libc/include/FileStream.hpp"
#include "prx/libc/include/general/VabiMacros.hpp"
#include <cerrno>
#include <cstdio>
#if defined(__linux__)
#include <stdio_ext.h>
#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<UcrtStreamState*>(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;
}
+128
View File
@@ -0,0 +1,128 @@
#include "prx/libc/include/Api.hpp"
#include "prx/libc/include/FileStream.hpp"
#include "prx/libc/include/general/VabiMacros.hpp"
#include <array>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <stdexcept>
#ifdef _WIN32
#include <io.h>
#include <fcntl.h>
#else
#include <unistd.h>
#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<char, 4096> 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<char, 4096> 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");
}
+2
View File
@@ -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.