Files
AnyPS5/core/libs/tests/FileStreams.cpp
T
xXPBSXxandClaude Sonnet 5.5 e5df7a3083 fix(libc): leave the error flag alone on the fread overflow and keep the fwrite throw
The console's libc.prx (read from PPSA21564 by the maintainer) returns from
fread with EINVAL before it touches the stream, so ferror stays clear, and
its fwrite has no overflow check at all. fread no longer sets the stream
error flag, fwrite keeps throwing, and the test follows.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-20 20:28:44 +02:00

237 lines
12 KiB
C++

#include "prx/libc/include/FileStream.hpp"
#include "prx/libc/include/general/VabiMacros.hpp"
#include <array>
#include <barrier>
#include <cerrno>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <iostream>
#include <stdexcept>
#include <string>
#include <thread>
#include <fcntl.h>
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <io.h>
#else
#include <unistd.h>
#endif
extern "C" {
FileStream* APS5_VABI fopen_nid_postfix(const char* filename, const char* mode);
FileStream* APS5_VABI _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(const char*, int, int);
FileStream* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode, FileStream* stream);
int APS5_VABI fclose_nid_postfix(FileStream* stream);
std::size_t APS5_VABI fread_nid_postfix(void* buffer, std::size_t size, std::size_t count, FileStream* stream);
std::size_t APS5_VABI fwrite_nid_postfix(const void* buffer, std::size_t size, std::size_t count, FileStream* stream);
int APS5_VABI fseek_nid_postfix(FileStream* stream, std::int64_t offset, int origin);
std::int64_t APS5_VABI ftell_nid_postfix(FileStream* stream);
int APS5_VABI fputs_nid_postfix(const char* str, FileStream* stream);
int APS5_VABI fflush_nid_postfix(FileStream* stream);
int APS5_VABI remove_nid_postfix(const char* path);
int APS5_VABI rename_nid_postfix(const char* from, const char* to);
int APS5_VABI ferror_nid_postfix(FileStream* stream);
void APS5_VABI clearerr_nid_postfix(FileStream* stream);
}
static void Require(bool condition, const char* message = "File stream check failed") {
if (!condition) throw std::runtime_error(message);
}
template<typename TAction>
static void ExpectException(TAction action) {
try {
action();
} catch (const std::runtime_error&) {
return;
}
throw std::runtime_error("Expected a stream exception");
}
static void ReplaceWithReadOnlyDescriptor(FileStream* stream, const char* path) {
#ifdef _WIN32
const int descriptor = _open(path, _O_RDONLY | _O_BINARY);
Require(descriptor >= 0 && _dup2(descriptor, _fileno(GetNativeStream(stream))) == 0);
Require(_close(descriptor) == 0);
#else
const int descriptor = open(path, O_RDONLY);
Require(descriptor >= 0 && dup2(descriptor, fileno(GetNativeStream(stream))) >= 0);
Require(close(descriptor) == 0);
#endif
}
static void CheckExclusiveCppOpen(const std::string& base) {
constexpr int Out = 0x02, Binary = 0x80, Noreplace = 0x40;
const std::string path = base + ".exclusive";
std::remove(path.c_str());
auto* stream = _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(path.c_str(), Out | Binary | Noreplace, 0);
Require(stream != nullptr);
constexpr char original[] = "keep existing contents";
Require(fwrite_nid_postfix(original, 1, sizeof(original), stream) == sizeof(original));
Require(fclose_nid_postfix(stream) == 0);
#ifdef _WIN32
const auto nativePath = std::filesystem::path(path).wstring();
const auto writer = CreateFileW(nativePath.c_str(), GENERIC_WRITE, FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
Require(writer != INVALID_HANDLE_VALUE);
auto* refused = _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(path.c_str(), Out | Binary | Noreplace, 0);
if (refused) fclose_nid_postfix(refused);
Require(CloseHandle(writer) != 0);
Require(refused == nullptr, "C++ noreplace truncated an existing write-shared file");
#endif
stream = fopen_nid_postfix(path.c_str(), "rb");
Require(stream != nullptr);
std::array<char, sizeof(original)> saved{};
Require(fread_nid_postfix(saved.data(), 1, saved.size(), stream) == saved.size());
Require(std::memcmp(saved.data(), original, saved.size()) == 0);
Require(fclose_nid_postfix(stream) == 0 && std::remove(path.c_str()) == 0);
for (const int mode : {0x42, 0x52, 0x53, 0xc2, 0xd2, 0xd3}) {
stream = _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(path.c_str(), mode | 0x04, 0);
Require(stream != nullptr && ftell_nid_postfix(stream) == 0);
Require(fwrite_nid_postfix(original, 1, sizeof(original), stream) == sizeof(original));
Require(fclose_nid_postfix(stream) == 0);
errno = 0;
Require(_ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(path.c_str(), mode, 0) == nullptr && errno == EEXIST);
stream = fopen_nid_postfix(path.c_str(), "rb");
Require(stream != nullptr && fread_nid_postfix(saved.data(), 1, saved.size(), stream) == saved.size());
Require(std::memcmp(saved.data(), original, saved.size()) == 0);
Require(fclose_nid_postfix(stream) == 0 && std::remove(path.c_str()) == 0);
}
for (int round = 0; round < 12; ++round) {
std::array<FileStream*, 2> created{};
std::barrier start(2);
const auto create = [&](int index) {
start.arrive_and_wait();
created[index] = _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(path.c_str(), Out | Binary | Noreplace, 0);
};
std::thread first(create, 0), second(create, 1);
first.join();
second.join();
const auto winners = static_cast<int>(created[0] != nullptr) + static_cast<int>(created[1] != nullptr);
for (auto* opened : created) if (opened) Require(fclose_nid_postfix(opened) == 0);
Require(winners == 1 && std::remove(path.c_str()) == 0);
}
}
int main(int argc, char** argv) {
Require(argc == 2);
CheckExclusiveCppOpen(argv[1]);
Require(_Stdout_nid_postfix.GetHandle() == stdout);
Require(_Stderr_nid_postfix.GetHandle() == stderr);
Require(fputs_nid_postfix("stdout object works\n", &_Stdout_nid_postfix) >= 0);
Require(fputs_nid_postfix("stderr object works\n", &_Stderr_nid_postfix) >= 0);
Require(fflush_nid_postfix(nullptr) == 0);
auto* stream = fopen_nid_postfix(argv[1], "w+b");
constexpr char payload[] = "stream round trip";
Require(fputs_nid_postfix(payload, stream) >= 0);
Require(fwrite_nid_postfix(payload, 1, sizeof(payload), stream) == sizeof(payload));
const auto expectedSize = sizeof(payload) * 2 - 1;
Require(ftell_nid_postfix(stream) == static_cast<long>(expectedSize));
Require(fflush_nid_postfix(stream) == 0);
Require(fseek_nid_postfix(stream, 0, SEEK_SET) == 0);
std::array<char, sizeof(payload)> buffer{};
Require(fread_nid_postfix(buffer.data(), 1, sizeof(payload) - 1, stream) == sizeof(payload) - 1);
Require(std::strcmp(buffer.data(), payload) == 0);
Require(fread_nid_postfix(buffer.data(), 1, buffer.size(), stream) == buffer.size());
Require(std::strcmp(buffer.data(), payload) == 0);
Require(fread_nid_postfix(buffer.data(), 1, buffer.size(), stream) == 0);
ExpectException([&] { fputs_nid_postfix(nullptr, stream); });
ExpectException([&] { fwrite_nid_postfix(nullptr, 1, sizeof(payload), stream); });
errno = 0;
Require(fread_nid_postfix(buffer.data(), 2, SIZE_MAX / 2 + 1, stream) == 0 && errno == EINVAL && ferror_nid_postfix(stream) == 0);
ExpectException([&] { fwrite_nid_postfix(buffer.data(), SIZE_MAX / 2 + 1, 2, stream); });
errno = 0;
Require(fseek_nid_postfix(stream, 0, -1) == -1);
Require(errno == EINVAL);
Require(fclose_nid_postfix(stream) == 0);
ExpectException([] { fputs_nid_postfix("invalid stream", nullptr); });
ExpectException([] { fopen_nid_postfix(nullptr, "r"); });
FileStream closed(std::tmpfile());
closed.Close();
Require(fflush_nid_postfix(&closed) == -1 && errno == 9);
Require(std::remove(argv[1]) == 0);
errno = 0;
Require(fopen_nid_postfix(argv[1], "rb") == nullptr);
Require(errno == ENOENT);
#ifdef _WIN32
const std::string directoryMode = "rb";
constexpr int directoryError = EACCES;
#else
const std::string directoryMode = "wb";
constexpr int directoryError = EISDIR;
#endif
const std::string directoryName = std::string(argv[1]) + ".dir";
Require(std::filesystem::create_directory(directoryName));
errno = 0;
Require(fopen_nid_postfix(directoryName.c_str(), directoryMode.c_str()) == nullptr);
Require(errno == directoryError);
Require(std::filesystem::remove(directoryName));
const std::u8string unicodeName = u8"セーブé.tmp";
const std::string guestName(unicodeName.begin(), unicodeName.end());
const std::filesystem::path hostName(unicodeName);
constexpr char unicodePayload[] = "unicode";
stream = fopen_nid_postfix(guestName.c_str(), "wb");
Require(stream != nullptr);
Require(fwrite_nid_postfix(unicodePayload, 1, sizeof(unicodePayload), stream) == sizeof(unicodePayload));
Require(fclose_nid_postfix(stream) == 0);
Require(std::filesystem::exists(hostName));
Require(std::filesystem::file_size(hostName) == sizeof(unicodePayload));
stream = fopen_nid_postfix(guestName.c_str(), "rb");
Require(stream != nullptr);
std::array<char, sizeof(unicodePayload)> unicodeBuffer{};
Require(fread_nid_postfix(unicodeBuffer.data(), 1, unicodeBuffer.size(), stream) == unicodeBuffer.size());
Require(std::strcmp(unicodeBuffer.data(), unicodePayload) == 0);
stream = freopen_nid_postfix(guestName.c_str(), "wb", stream);
Require(stream != nullptr);
Require(fclose_nid_postfix(stream) == 0);
Require(std::filesystem::file_size(hostName) == 0);
std::filesystem::remove(hostName);
const std::string heldName = std::string(argv[1]) + ".held";
const std::string movedName = std::string(argv[1]) + ".moved";
stream = fopen_nid_postfix(heldName.c_str(), "w+b");
Require(stream != nullptr && fwrite_nid_postfix(payload, 1, sizeof(payload), stream) == sizeof(payload));
Require(fflush_nid_postfix(stream) == 0);
Require(rename_nid_postfix(heldName.c_str(), movedName.c_str()) == 0 && !std::filesystem::exists(heldName));
Require(remove_nid_postfix(movedName.c_str()) == 0 && !std::filesystem::exists(movedName));
Require(fseek_nid_postfix(stream, 0, SEEK_SET) == 0);
std::array<char, sizeof(payload)> heldBuffer{};
Require(fread_nid_postfix(heldBuffer.data(), 1, heldBuffer.size(), stream) == heldBuffer.size());
Require(std::strcmp(heldBuffer.data(), payload) == 0 && fclose_nid_postfix(stream) == 0);
stream = fopen_nid_postfix(heldName.c_str(), "ab");
Require(stream != nullptr && fputs_nid_postfix("ab", stream) >= 0 && fclose_nid_postfix(stream) == 0);
stream = fopen_nid_postfix(heldName.c_str(), "ab");
Require(stream != nullptr && fputs_nid_postfix("cd", stream) >= 0 && fclose_nid_postfix(stream) == 0);
errno = 0;
Require(fopen_nid_postfix(heldName.c_str(), "wbx") == nullptr && errno == EEXIST);
stream = fopen_nid_postfix(heldName.c_str(), "rb");
std::array<char, 5> appended{};
Require(stream != nullptr && fread_nid_postfix(appended.data(), 1, 4, stream) == 4 && std::strcmp(appended.data(), "abcd") == 0);
Require(fclose_nid_postfix(stream) == 0 && remove_nid_postfix(heldName.c_str()) == 0);
constexpr char marker[] = "marker";
stream = fopen_nid_postfix(argv[1], "wb");
Require(stream != nullptr);
std::array<char, sizeof(marker)> markerBuffer{};
Require(fread_nid_postfix(markerBuffer.data(), 1, markerBuffer.size(), stream) == 0);
Require(ferror_nid_postfix(stream) != 0);
Require(fclose_nid_postfix(stream) == 0);
stream = fopen_nid_postfix(argv[1], "rb");
Require(stream != nullptr);
Require(fwrite_nid_postfix(marker, 1, sizeof(marker), stream) == 0);
Require(ferror_nid_postfix(stream) != 0);
clearerr_nid_postfix(stream);
Require(fputs_nid_postfix(marker, stream) == EOF);
Require(ferror_nid_postfix(stream) != 0);
Require(fclose_nid_postfix(stream) == 0);
stream = fopen_nid_postfix(argv[1], "wb");
Require(stream != nullptr && fputs_nid_postfix(marker, stream) >= 0);
ReplaceWithReadOnlyDescriptor(stream, argv[1]);
Require(fflush_nid_postfix(stream) == EOF);
Require(ferror_nid_postfix(stream) != 0);
static_cast<void>(fclose_nid_postfix(stream));
Require(std::remove(argv[1]) == 0);
std::cout << "PASS: stream objects, file operations, EOF and error handling\n";
}