Files
AnyPS5/core/libs/tests/FileStreams.cpp
T
Ethan Barr 4f6a37d790 fix(libc): share delete access when freopen reopens a stream onto a file on Windows
On Windows, freopen reopened a stream through _wfreopen, which opens the file without FILE_SHARE_DELETE, so remove or rename of the file failed while the stream was open, unlike fopen streams and POSIX unlink/rename of an open file. freopen now closes the previous stream and opens the new file through the same CreateFileW path fopen uses, so the reopened stream shares delete access. Host standard streams and an empty filename keep the _wfreopen path. Reference: FreeBSD 11 unlink(2) and rename(2) succeed on a file that is open.
2026-10-20 17:07:21 -04:00

244 lines
13 KiB
C++

#include "prx/libc/include/FileStream.hpp"
#include "prx/libc/include/general/VabiMacros.hpp"
#include <array>
#include <barrier>
#include <cerrno>
#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(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);
stream = fopen_nid_postfix(heldName.c_str(), "w+b");
Require(stream != nullptr);
stream = freopen_nid_postfix(movedName.c_str(), "w+b", stream);
Require(stream != nullptr && fwrite_nid_postfix(payload, 1, sizeof(payload), stream) == sizeof(payload));
Require(fflush_nid_postfix(stream) == 0);
Require(rename_nid_postfix(movedName.c_str(), heldName.c_str()) == 0 && !std::filesystem::exists(movedName));
Require(remove_nid_postfix(heldName.c_str()) == 0 && !std::filesystem::exists(heldName));
Require(fseek_nid_postfix(stream, 0, SEEK_SET) == 0);
std::array<char, sizeof(payload)> reopenedBuffer{};
Require(fread_nid_postfix(reopenedBuffer.data(), 1, reopenedBuffer.size(), stream) == reopenedBuffer.size());
Require(std::strcmp(reopenedBuffer.data(), payload) == 0 && fclose_nid_postfix(stream) == 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";
}