Files
FEX-Emu--FEX/FEXCore/include/FEXCore/Utils/File.h
T
Lioncache aaca8772bd Utils/File: Properly handle move assignment/construction
Ensures that whenever a file handle is transferred anywhere, that the
moved from instance won't end up closing the file handle when the
destructor runs.
2026-09-01 16:00:11 -04:00

452 lines
11 KiB
C++

// SPDX-License-Identifier: MIT
#pragma once
#include <FEXCore/fextl/allocator.h>
#include <FEXCore/fextl/string.h>
#include <FEXCore/Utils/EnumOperators.h>
#include "FEXCore/Utils/LogManager.h"
#include <chrono>
#include <thread>
#include <utility>
#ifndef _WIN32
#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>
#include <sys/stat.h>
#else
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#undef ERROR
#endif
namespace FEXCore::File {
enum class FileModes : uint32_t {
READ = (1U << 0),
WRITE = (1U << 1),
CREATE = (1U << 2),
TRUNCATE = (1U << 3),
};
enum class SeekOp {
BEGIN,
CURRENT,
END,
};
FEX_DEF_NUM_OPS(FileModes)
class File final {
public:
#ifndef _WIN32
using FileHandleType = int;
#else
using FileHandleType = HANDLE;
#endif
File() = default;
File(const char* Filepath, FileModes Modes, bool Seekable = true)
: Seekable {Seekable} {
#ifndef _WIN32
auto Disp = TranslateModes(Modes);
Handle = open(Filepath, Disp, DEFAULT_USER_PERMS);
IsValidHandle = Handle != -1;
#else
auto Disp = TranslateModes(Modes);
if (Disp.CreationFlag == OPEN_ALWAYS && Disp.TruncateOnExist) {
// If Open + Truncate then try to open with truncate behaviour first.
Handle = CreateFileA(Filepath, Disp.Access, DEFAULT_SHARE_MODE, nullptr, TRUNCATE_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (Handle == INVALID_HANDLE_VALUE && GetLastError() == ERROR_FILE_NOT_FOUND) {
// File didn't exist, just open.
Handle = CreateFileA(Filepath, Disp.Access, DEFAULT_SHARE_MODE, nullptr, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr);
}
} else {
Handle = CreateFileA(Filepath, Disp.Access, DEFAULT_SHARE_MODE, nullptr, Disp.CreationFlag, FILE_ATTRIBUTE_NORMAL, nullptr);
}
IsValidHandle = Handle != INVALID_HANDLE_VALUE;
#endif
ShouldClose = IsValidHandle;
}
File(File&& Other) noexcept
: ShouldClose(std::exchange(Other.ShouldClose, false))
, IsValidHandle(std::exchange(Other.IsValidHandle, false))
#ifdef _WIN32
, Handle(std::exchange(Other.Handle, INVALID_HANDLE_VALUE))
#else
, Handle(std::exchange(Other.Handle, -1))
#endif
, Seekable(std::exchange(Other.Seekable, false))
, Locked(std::exchange(Other.Locked, false)) {
}
File& operator=(File&& Other) noexcept {
if (this == &Other) {
return *this;
}
std::swap(ShouldClose, Other.ShouldClose);
std::swap(IsValidHandle, Other.IsValidHandle);
std::swap(Handle, Other.Handle);
std::swap(Seekable, Other.Seekable);
std::swap(Locked, Other.Locked);
return *this;
}
File(const File&) = delete;
File& operator=(const File&) = delete;
FileHandleType GetHandle() {
return Handle;
}
/**
* @brief Write Bytes to File
*
* @param Buffer The buffer to write.
* @param Bytes The number of bytes to write.
*
* @return The number of bytes actually written or -1 on error.
*/
ssize_t Write(const void* Buffer, size_t Bytes) {
if (!Seekable) {
LOGMAN_THROW_A_FMT(false, "Can't use non-positioned ops on a non-seekable file!");
return -1;
}
#ifndef _WIN32
return write(Handle, Buffer, Bytes);
#else
DWORD BytesWritten {};
auto Result = WriteFile(Handle, Buffer, Bytes, &BytesWritten, nullptr);
if (Result) {
return BytesWritten;
}
// Some error, match Linux side.
return -1;
#endif
}
ssize_t Write(const std::string_view Data) {
return Write(Data.data(), Data.size());
}
/**
* @brief Read at most Bytes in to the buffer.
*
* @param Buffer The buffer where the data is read in to.
* @param Bytes The size of the buffer.
*
* @return The number of bytes read or -1 on error.
*/
ssize_t Read(void* Buffer, size_t Bytes) {
if (!Seekable) {
LOGMAN_THROW_A_FMT(false, "Can't use non-positioned ops on a non-seekable file!");
return -1;
}
#ifndef _WIN32
return read(Handle, Buffer, Bytes);
#else
DWORD BytesRead {};
auto Result = ReadFile(Handle, Buffer, Bytes, &BytesRead, nullptr);
if (Result) {
return BytesRead;
}
// Some error, match Linux side.
return -1;
#endif
}
ssize_t PRead(void* Buffer, size_t Bytes, uint64_t Offset) {
if (Seekable) {
LOGMAN_THROW_A_FMT(false, "Can't use positioned ops on a seekable file!");
return -1;
}
#ifndef _WIN32
return pread(Handle, Buffer, Bytes, Offset);
#else
DWORD BytesRead {};
OVERLAPPED Overlapped {};
Overlapped.Offset = static_cast<DWORD>(Offset);
Overlapped.OffsetHigh = static_cast<DWORD>(Offset >> 32);
auto Result = ReadFile(Handle, Buffer, Bytes, &BytesRead, &Overlapped);
if (Result) {
return BytesRead;
}
// Some error, match Linux side.
return -1;
#endif
}
ssize_t PWrite(const void* Buffer, size_t Bytes, uint64_t Offset) {
if (Seekable) {
LOGMAN_THROW_A_FMT(false, "Can't use positioned ops on a seekable file!");
return -1;
}
#ifndef _WIN32
return pwrite(Handle, Buffer, Bytes, Offset);
#else
DWORD BytesWritten {};
OVERLAPPED Overlapped {};
Overlapped.Offset = static_cast<DWORD>(Offset);
Overlapped.OffsetHigh = static_cast<DWORD>(Offset >> 32);
auto Result = WriteFile(Handle, Buffer, Bytes, &BytesWritten, &Overlapped);
if (Result) {
return BytesWritten;
}
// Some error, match Linux side.
return -1;
#endif
}
bool Lock(uint32_t TimeoutMS) {
for (uint32_t i = 0;; ++i) {
if (TryLock()) {
return true;
}
if (i >= TimeoutMS) {
return false;
}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
bool Unlock() {
if (!Locked) {
return false; // we could return true here :thonk:
}
#ifndef _WIN32
if (flock(Handle, LOCK_UN) == -1) {
return false;
}
#else
OVERLAPPED Overlapped {};
Overlapped.Offset = static_cast<DWORD>(LOCK_SENTINEL_OFFSET);
Overlapped.OffsetHigh = static_cast<DWORD>(LOCK_SENTINEL_OFFSET >> 32);
if (!UnlockFileEx(Handle, 0, 1, 0, &Overlapped)) {
return false;
}
#endif
Locked = false;
return true;
}
~File() {
if (!IsValidHandle) {
return;
}
if (!ShouldClose) {
return;
}
#ifndef _WIN32
close(Handle);
#else
CloseHandle(Handle);
#endif
}
/**
* @brief Gets a File object that points to stdout
*/
static File GetStdOUT() {
#ifndef _WIN32
return File(STDOUT_FILENO, false);
#else
return File(GetStdHandle(STD_OUTPUT_HANDLE), false);
#endif
}
/**
* @brief Gets a File object that points to stderr
*/
static File GetStdERR() {
#ifndef _WIN32
return File(STDERR_FILENO, false);
#else
return File(GetStdHandle(STD_ERROR_HANDLE), false);
#endif
}
/**
* @brief Returns if the file handle is valid.
*/
bool IsValid() const {
return IsValidHandle;
}
/**
* @brief Flush the file contents to the output file backing.
*
* @return True if the flush occured.
*/
bool Flush() {
#ifndef _WIN32
return fsync(Handle) == 0;
#else
return FlushFileBuffers(Handle);
#endif
}
ssize_t Size() {
#ifndef _WIN32
struct stat st;
if (fstat(Handle, &st) != 0) {
return -1;
}
return st.st_size;
#else
LARGE_INTEGER FileSize;
if (!GetFileSizeEx(Handle, &FileSize)) {
return -1;
}
return FileSize.QuadPart;
#endif
}
/**
* @brief Seek the file pointer location.
*
* @param Distance The distance to travel.
* @param Op The operation from where to start the travel.
*
* @return The current file pointer location or -1.
*/
ssize_t Seek(ssize_t Distance, SeekOp Op) {
if (!Seekable) {
LOGMAN_THROW_A_FMT(false, "Can't use non-positioned ops on a non-seekable file!");
return -1;
}
#ifndef _WIN32
return lseek(Handle, Distance, TranslateSeek(Op));
#else
LARGE_INTEGER NewDistance {.QuadPart = Distance};
LARGE_INTEGER NewPointer;
auto Result = SetFilePointerEx(Handle, NewDistance, &NewPointer, TranslateSeek(Op));
if (Result) {
return NewPointer.QuadPart;
}
// Some error, match Linux side.
return -1;
#endif
}
protected:
File(FileHandleType Handle, bool ShouldClose, bool Seekable = true)
: ShouldClose {ShouldClose}
, IsValidHandle {true}
, Handle {Handle}
, Seekable {Seekable} {}
private:
bool TryLock() {
if (Locked) {
return true;
}
#ifndef _WIN32
if (flock(Handle, LOCK_EX | LOCK_NB) == -1) {
return false;
}
#else
// mimic posix advisory-only lock by locking some unattainably-high bit
OVERLAPPED Overlapped {};
Overlapped.Offset = static_cast<DWORD>(LOCK_SENTINEL_OFFSET);
Overlapped.OffsetHigh = static_cast<DWORD>(LOCK_SENTINEL_OFFSET >> 32);
if (!LockFileEx(Handle, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY, 0, 1, 0, &Overlapped)) {
return false;
}
#endif
Locked = true;
return true;
}
bool ShouldClose {};
bool IsValidHandle {};
FileHandleType Handle {};
bool Seekable = true;
bool Locked = false;
#ifndef _WIN32
static constexpr int DEFAULT_USER_PERMS = S_IRWXU | S_IRWXG | S_IRWXO;
static uint32_t TranslateModes(FileModes Modes) {
const auto IsRead = (Modes & FileModes::READ) == FileModes::READ;
const auto IsWrite = (Modes & FileModes::WRITE) == FileModes::WRITE;
uint32_t Mode {};
if (IsRead && IsWrite) {
Mode |= O_RDWR;
} else {
if (IsRead) {
Mode |= O_RDONLY;
} else if (IsWrite) {
Mode |= O_WRONLY;
}
}
if ((Modes & FileModes::CREATE) == FileModes::CREATE) {
Mode |= O_CREAT;
}
if ((Modes & FileModes::TRUNCATE) == FileModes::TRUNCATE) {
Mode |= O_TRUNC;
}
// Always enable CLOEXEC so that the FD is closed on execve.
// FEXCore never wants to leak FDs across execve using this interface.
Mode |= O_CLOEXEC;
return Mode;
}
static uint32_t TranslateSeek(SeekOp Op) {
switch (Op) {
case SeekOp::BEGIN: return SEEK_SET;
case SeekOp::CURRENT: return SEEK_CUR;
case SeekOp::END: return SEEK_END;
default: FEX_UNREACHABLE;
}
}
#else
static constexpr int DEFAULT_SHARE_MODE = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE;
static constexpr uint64_t LOCK_SENTINEL_OFFSET = 1ULL << 62;
struct Disposition {
uint32_t CreationFlag;
uint32_t Access;
bool TruncateOnExist;
};
static Disposition TranslateModes(FileModes Modes) {
Disposition Disp {};
if ((Modes & FileModes::READ) == FileModes::READ) {
Disp.Access |= GENERIC_READ;
}
if ((Modes & FileModes::WRITE) == FileModes::WRITE) {
Disp.Access |= GENERIC_WRITE;
}
if ((Modes & FileModes::CREATE) == FileModes::CREATE) {
if ((Modes & FileModes::TRUNCATE) == FileModes::TRUNCATE) {
Disp.CreationFlag = CREATE_ALWAYS;
} else {
Disp.CreationFlag = OPEN_ALWAYS;
}
} else {
Disp.CreationFlag = OPEN_ALWAYS;
}
if ((Modes & FileModes::TRUNCATE) == FileModes::TRUNCATE) {
Disp.TruncateOnExist = true;
}
return Disp;
}
static uint32_t TranslateSeek(SeekOp Op) {
switch (Op) {
case SeekOp::BEGIN: return FILE_BEGIN;
case SeekOp::CURRENT: return FILE_CURRENT;
case SeekOp::END: return FILE_END;
default: FEX_UNREACHABLE;
}
}
#endif
};
} // namespace FEXCore::File