mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-12-16 04:09:39 +00:00
Common: Add a DirectIOFile class that allows for copies which are entirely thread safe.
This commit is contained in:
parent
db997e9963
commit
405baed805
@ -52,6 +52,8 @@ add_library(common
|
|||||||
Debug/Threads.h
|
Debug/Threads.h
|
||||||
Debug/Watches.cpp
|
Debug/Watches.cpp
|
||||||
Debug/Watches.h
|
Debug/Watches.h
|
||||||
|
DirectIOFile.cpp
|
||||||
|
DirectIOFile.h
|
||||||
DynamicLibrary.cpp
|
DynamicLibrary.cpp
|
||||||
DynamicLibrary.h
|
DynamicLibrary.h
|
||||||
ENet.cpp
|
ENet.cpp
|
||||||
|
|||||||
372
Source/Core/Common/DirectIOFile.cpp
Normal file
372
Source/Core/Common/DirectIOFile.cpp
Normal file
@ -0,0 +1,372 @@
|
|||||||
|
// Copyright 2025 Dolphin Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "Common/DirectIOFile.h"
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include "Common/Buffer.h"
|
||||||
|
#include "Common/CommonFuncs.h"
|
||||||
|
#include "Common/MathUtil.h"
|
||||||
|
#include "Common/StringUtil.h"
|
||||||
|
#else
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#ifdef ANDROID
|
||||||
|
#include "jni/AndroidCommon/AndroidCommon.h"
|
||||||
|
|
||||||
|
#include "Common/Lazy.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "Common/FileUtil.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "Common/Assert.h"
|
||||||
|
|
||||||
|
namespace File
|
||||||
|
{
|
||||||
|
DirectIOFile::DirectIOFile() = default;
|
||||||
|
|
||||||
|
DirectIOFile::~DirectIOFile()
|
||||||
|
{
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
DirectIOFile::DirectIOFile(const DirectIOFile& other)
|
||||||
|
{
|
||||||
|
*this = other.Duplicate();
|
||||||
|
}
|
||||||
|
|
||||||
|
DirectIOFile& DirectIOFile::operator=(const DirectIOFile& other)
|
||||||
|
{
|
||||||
|
return *this = other.Duplicate();
|
||||||
|
}
|
||||||
|
|
||||||
|
DirectIOFile::DirectIOFile(DirectIOFile&& other)
|
||||||
|
{
|
||||||
|
Swap(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
DirectIOFile& DirectIOFile::operator=(DirectIOFile&& other)
|
||||||
|
{
|
||||||
|
Close();
|
||||||
|
Swap(other);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
DirectIOFile::DirectIOFile(const std::string& path, AccessMode access_mode, OpenMode open_mode)
|
||||||
|
{
|
||||||
|
Open(path, access_mode, open_mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DirectIOFile::Open(const std::string& path, AccessMode access_mode, OpenMode open_mode)
|
||||||
|
{
|
||||||
|
ASSERT(!IsOpen());
|
||||||
|
|
||||||
|
if (open_mode == OpenMode::Default)
|
||||||
|
open_mode = (access_mode == AccessMode::Write) ? OpenMode::Truncate : OpenMode::Existing;
|
||||||
|
|
||||||
|
// This is not a sensible combination. Fail here to not rely on OS-specific behaviors.
|
||||||
|
if (access_mode == AccessMode::Read && open_mode == OpenMode::Truncate)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
DWORD desired_access = GENERIC_READ | GENERIC_WRITE;
|
||||||
|
if (access_mode == AccessMode::Read)
|
||||||
|
desired_access = GENERIC_READ;
|
||||||
|
else if (access_mode == AccessMode::Write)
|
||||||
|
desired_access = GENERIC_WRITE;
|
||||||
|
|
||||||
|
// Allow deleting and renaming through our handle.
|
||||||
|
desired_access |= DELETE;
|
||||||
|
|
||||||
|
// All sharing is allowed to more closely match default behavior on other OSes.
|
||||||
|
constexpr DWORD share_mode = FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE;
|
||||||
|
|
||||||
|
DWORD creation_disposition = OPEN_ALWAYS;
|
||||||
|
if (open_mode == OpenMode::Truncate)
|
||||||
|
creation_disposition = CREATE_ALWAYS;
|
||||||
|
else if (open_mode == OpenMode::Create)
|
||||||
|
creation_disposition = CREATE_NEW;
|
||||||
|
else if (open_mode == OpenMode::Existing)
|
||||||
|
creation_disposition = OPEN_EXISTING;
|
||||||
|
|
||||||
|
m_handle = CreateFile(UTF8ToTStr(path).c_str(), desired_access, share_mode, nullptr,
|
||||||
|
creation_disposition, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||||
|
if (!IsOpen())
|
||||||
|
WARN_LOG_FMT(COMMON, "CreateFile: {}", Common::GetLastErrorString());
|
||||||
|
|
||||||
|
#else
|
||||||
|
#if defined(ANDROID)
|
||||||
|
if (IsPathAndroidContent(path))
|
||||||
|
{
|
||||||
|
// Android documentation says that "w" may or may not truncate.
|
||||||
|
// In case it does, we'll use "rw" when we don't want truncation.
|
||||||
|
// This wrongly enables read access when we don't need it.
|
||||||
|
if (access_mode == AccessMode::Write && open_mode != OpenMode::Truncate)
|
||||||
|
access_mode = AccessMode::ReadAndWrite;
|
||||||
|
|
||||||
|
std::string open_mode_str = "rw";
|
||||||
|
if (access_mode == AccessMode::Read)
|
||||||
|
open_mode_str = "r";
|
||||||
|
else if (access_mode == AccessMode::Write)
|
||||||
|
open_mode_str = "w";
|
||||||
|
|
||||||
|
// FYI: File::Exists can be slow on Android.
|
||||||
|
Common::Lazy<bool> file_exists{[&] { return Exists(path); }};
|
||||||
|
|
||||||
|
// A few features are emulated in a non-atomic manner.
|
||||||
|
if (open_mode == OpenMode::Existing)
|
||||||
|
{
|
||||||
|
if (access_mode != AccessMode::Read && !*file_exists)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (open_mode == OpenMode::Truncate)
|
||||||
|
open_mode_str += 't';
|
||||||
|
else if (open_mode == OpenMode::Create && *file_exists)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Modes other than `Existing` may create a file, but "r" won't do that automatically.
|
||||||
|
if (access_mode == AccessMode::Read && !*file_exists)
|
||||||
|
CreateEmptyFile(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_fd = OpenAndroidContent(path, open_mode_str);
|
||||||
|
|
||||||
|
return IsOpen();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
int flags = O_RDWR;
|
||||||
|
if (access_mode == AccessMode::Read)
|
||||||
|
flags = O_RDONLY;
|
||||||
|
else if (access_mode == AccessMode::Write)
|
||||||
|
flags = O_WRONLY;
|
||||||
|
|
||||||
|
if (open_mode == OpenMode::Truncate)
|
||||||
|
flags |= O_CREAT | O_TRUNC;
|
||||||
|
else if (open_mode == OpenMode::Create)
|
||||||
|
flags |= O_CREAT | O_EXCL;
|
||||||
|
else if (open_mode != OpenMode::Existing)
|
||||||
|
flags |= O_CREAT;
|
||||||
|
|
||||||
|
m_fd = open(path.c_str(), flags, 0666);
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return IsOpen();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DirectIOFile::Close()
|
||||||
|
{
|
||||||
|
if (!IsOpen())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
m_current_offset = 0;
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
return CloseHandle(std::exchange(m_handle, INVALID_HANDLE_VALUE)) != 0;
|
||||||
|
#else
|
||||||
|
return close(std::exchange(m_fd, -1)) == 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DirectIOFile::IsOpen() const
|
||||||
|
{
|
||||||
|
#if defined(_WIN32)
|
||||||
|
return m_handle != INVALID_HANDLE_VALUE;
|
||||||
|
#else
|
||||||
|
return m_fd != -1;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
template <auto* TransferFunc>
|
||||||
|
static bool OverlappedTransfer(HANDLE handle, u64 offset, auto* data_ptr, u64 size)
|
||||||
|
{
|
||||||
|
// ReadFile/WriteFile take a 32bit size so we must loop to handle our 64bit size.
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
OVERLAPPED overlapped{};
|
||||||
|
overlapped.Offset = DWORD(offset);
|
||||||
|
overlapped.OffsetHigh = DWORD(offset >> 32);
|
||||||
|
|
||||||
|
DWORD bytes_transferred{};
|
||||||
|
if (TransferFunc(handle, data_ptr, MathUtil::SaturatingCast<DWORD>(size), &bytes_transferred,
|
||||||
|
&overlapped) == 0)
|
||||||
|
{
|
||||||
|
ERROR_LOG_FMT(COMMON, "OverlappedTransfer: {}", Common::GetLastErrorString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
size -= bytes_transferred;
|
||||||
|
|
||||||
|
if (size == 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
offset += bytes_transferred;
|
||||||
|
data_ptr += bytes_transferred;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
bool DirectIOFile::OffsetRead(u64 offset, u8* out_ptr, u64 size)
|
||||||
|
{
|
||||||
|
#if defined(_WIN32)
|
||||||
|
return OverlappedTransfer<ReadFile>(m_handle, offset, out_ptr, size);
|
||||||
|
#else
|
||||||
|
return pread(m_fd, out_ptr, size, off_t(offset)) == ssize_t(size);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DirectIOFile::OffsetWrite(u64 offset, const u8* in_ptr, u64 size)
|
||||||
|
{
|
||||||
|
#if defined(_WIN32)
|
||||||
|
return OverlappedTransfer<WriteFile>(m_handle, offset, in_ptr, size);
|
||||||
|
#else
|
||||||
|
return pwrite(m_fd, in_ptr, size, off_t(offset)) == ssize_t(size);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 DirectIOFile::GetSize() const
|
||||||
|
{
|
||||||
|
#if defined(_WIN32)
|
||||||
|
LARGE_INTEGER result{};
|
||||||
|
if (GetFileSizeEx(m_handle, &result) != 0)
|
||||||
|
return result.QuadPart;
|
||||||
|
#else
|
||||||
|
struct stat st{};
|
||||||
|
if (fstat(m_fd, &st) == 0)
|
||||||
|
return st.st_size;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DirectIOFile::Seek(s64 offset, SeekOrigin origin)
|
||||||
|
{
|
||||||
|
if (!IsOpen())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
u64 reference_pos = 0;
|
||||||
|
switch (origin)
|
||||||
|
{
|
||||||
|
case SeekOrigin::Current:
|
||||||
|
reference_pos = m_current_offset;
|
||||||
|
break;
|
||||||
|
case SeekOrigin::End:
|
||||||
|
reference_pos = GetSize();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't let our current offset underflow.
|
||||||
|
if (offset < 0 && u64(-offset) > reference_pos)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
m_current_offset = reference_pos + offset;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DirectIOFile::Flush()
|
||||||
|
{
|
||||||
|
#if defined(_WIN32)
|
||||||
|
return FlushFileBuffers(m_handle) != 0;
|
||||||
|
#else
|
||||||
|
return fsync(m_fd) == 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void DirectIOFile::Swap(DirectIOFile& other)
|
||||||
|
{
|
||||||
|
#if defined(_WIN32)
|
||||||
|
std::swap(m_handle, other.m_handle);
|
||||||
|
#else
|
||||||
|
std::swap(m_fd, other.m_fd);
|
||||||
|
#endif
|
||||||
|
std::swap(m_current_offset, other.m_current_offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
DirectIOFile DirectIOFile::Duplicate() const
|
||||||
|
{
|
||||||
|
DirectIOFile result;
|
||||||
|
|
||||||
|
if (!IsOpen())
|
||||||
|
return result;
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
const auto current_process = GetCurrentProcess();
|
||||||
|
if (DuplicateHandle(current_process, m_handle, current_process, &result.m_handle, 0, FALSE,
|
||||||
|
DUPLICATE_SAME_ACCESS) == 0)
|
||||||
|
{
|
||||||
|
ERROR_LOG_FMT(COMMON, "DuplicateHandle: {}", Common::GetLastErrorString());
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
result.m_fd = dup(m_fd);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
ASSERT(result.IsOpen());
|
||||||
|
|
||||||
|
result.m_current_offset = m_current_offset;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Resize(DirectIOFile& file, u64 size)
|
||||||
|
{
|
||||||
|
#if defined(_WIN32)
|
||||||
|
// This operation is not "atomic", but it's the only thing we're using the file pointer for.
|
||||||
|
// Concurrent `Resize` would need some external synchronization to prevent race regardless.
|
||||||
|
const LARGE_INTEGER distance{.QuadPart = LONGLONG(size)};
|
||||||
|
return (SetFilePointerEx(file.GetHandle(), distance, nullptr, FILE_BEGIN) != 0) &&
|
||||||
|
(SetEndOfFile(file.GetHandle()) != 0);
|
||||||
|
#else
|
||||||
|
return ftruncate(file.GetHandle(), off_t(size)) == 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Rename(DirectIOFile& file, const std::string& source_path [[maybe_unused]],
|
||||||
|
const std::string& destination_path)
|
||||||
|
{
|
||||||
|
#if defined(_WIN32)
|
||||||
|
const auto dest_name = UTF8ToWString(destination_path);
|
||||||
|
const auto dest_name_byte_size = DWORD(dest_name.size() * sizeof(WCHAR));
|
||||||
|
FILE_RENAME_INFO info{
|
||||||
|
.ReplaceIfExists = TRUE,
|
||||||
|
.FileNameLength = dest_name_byte_size, // The size in bytes, not including null termination.
|
||||||
|
};
|
||||||
|
constexpr auto filename_struct_offset = offsetof(FILE_RENAME_INFO, FileName);
|
||||||
|
Common::UniqueBuffer<u8> buffer(filename_struct_offset + dest_name_byte_size + sizeof(WCHAR));
|
||||||
|
std::memcpy(buffer.data(), &info, filename_struct_offset);
|
||||||
|
std::memcpy(buffer.data() + filename_struct_offset, dest_name.c_str(),
|
||||||
|
dest_name_byte_size + sizeof(WCHAR));
|
||||||
|
return SetFileInformationByHandle(file.GetHandle(), FileRenameInfo, buffer.data(),
|
||||||
|
DWORD(buffer.size())) != 0;
|
||||||
|
#else
|
||||||
|
return file.IsOpen() && Rename(source_path, destination_path);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Delete(DirectIOFile& file, const std::string& filename)
|
||||||
|
{
|
||||||
|
#if defined(_WIN32)
|
||||||
|
FILE_DISPOSITION_INFO info{.DeleteFile = TRUE};
|
||||||
|
return SetFileInformationByHandle(file.GetHandle(), FileDispositionInfo, &info, sizeof(info)) !=
|
||||||
|
0;
|
||||||
|
#else
|
||||||
|
return file.IsOpen() && Delete(filename, IfAbsentBehavior::NoConsoleWarning);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace File
|
||||||
150
Source/Core/Common/DirectIOFile.h
Normal file
150
Source/Core/Common/DirectIOFile.h
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
// Copyright 2025 Dolphin Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <span>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "Common/CommonTypes.h"
|
||||||
|
#include "Common/IOFile.h"
|
||||||
|
|
||||||
|
namespace File
|
||||||
|
{
|
||||||
|
enum class AccessMode
|
||||||
|
{
|
||||||
|
Read,
|
||||||
|
Write,
|
||||||
|
ReadAndWrite,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class OpenMode
|
||||||
|
{
|
||||||
|
// Based on the provided `AccessMode`.
|
||||||
|
// Read: Existing.
|
||||||
|
// Write: Truncate.
|
||||||
|
// ReadAndWrite: Existing.
|
||||||
|
Default,
|
||||||
|
|
||||||
|
// Either create a new file or open an existing file.
|
||||||
|
Always,
|
||||||
|
|
||||||
|
// Like `Always`, but also erase the contents of an existing file.
|
||||||
|
Truncate,
|
||||||
|
|
||||||
|
// Require a file to already exist. Fail otherwise.
|
||||||
|
Existing,
|
||||||
|
|
||||||
|
// Require a new file be created. Fail if one already exists.
|
||||||
|
Create,
|
||||||
|
};
|
||||||
|
|
||||||
|
// This file wrapper avoids use of the underlying system file position.
|
||||||
|
// It keeps track of its own file position and read/write calls directly use it.
|
||||||
|
// This makes copied handles entirely thread safe.
|
||||||
|
class DirectIOFile final
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
DirectIOFile();
|
||||||
|
~DirectIOFile();
|
||||||
|
|
||||||
|
// Copies and moves are allowed.
|
||||||
|
DirectIOFile(const DirectIOFile&);
|
||||||
|
DirectIOFile& operator=(const DirectIOFile&);
|
||||||
|
DirectIOFile(DirectIOFile&&);
|
||||||
|
DirectIOFile& operator=(DirectIOFile&&);
|
||||||
|
|
||||||
|
explicit DirectIOFile(const std::string& path, AccessMode access_mode,
|
||||||
|
OpenMode open_mode = OpenMode::Default);
|
||||||
|
|
||||||
|
bool Open(const std::string& path, AccessMode access_mode,
|
||||||
|
OpenMode open_mode = OpenMode::Default);
|
||||||
|
|
||||||
|
bool Close();
|
||||||
|
|
||||||
|
bool IsOpen() const;
|
||||||
|
|
||||||
|
// An offset from the start of the file may be specified directly.
|
||||||
|
// These explicit offset versions entirely ignore the current file position.
|
||||||
|
// They are thread safe, even when used on the same object.
|
||||||
|
|
||||||
|
bool OffsetRead(u64 offset, u8* out_ptr, u64 size);
|
||||||
|
bool OffsetRead(u64 offset, std::span<u8> out_data)
|
||||||
|
{
|
||||||
|
return OffsetRead(offset, out_data.data(), out_data.size());
|
||||||
|
}
|
||||||
|
bool OffsetWrite(u64 offset, const u8* in_ptr, u64 size);
|
||||||
|
bool OffsetWrite(auto offset, std::span<const u8> in_data)
|
||||||
|
{
|
||||||
|
return OffsetWrite(offset, in_data.data(), in_data.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
// These Read/Write functions advance the current position on success.
|
||||||
|
|
||||||
|
bool Read(u8* out_ptr, u64 size)
|
||||||
|
{
|
||||||
|
if (!OffsetRead(m_current_offset, out_ptr, size))
|
||||||
|
return false;
|
||||||
|
m_current_offset += size;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
bool Read(std::span<u8> out_data) { return Read(out_data.data(), out_data.size()); }
|
||||||
|
|
||||||
|
bool Write(const u8* in_ptr, u64 size)
|
||||||
|
{
|
||||||
|
if (!OffsetWrite(m_current_offset, in_ptr, size))
|
||||||
|
return false;
|
||||||
|
m_current_offset += size;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
bool Write(std::span<const u8> in_data) { return Write(in_data.data(), in_data.size()); }
|
||||||
|
|
||||||
|
// Returns 0 on error.
|
||||||
|
u64 GetSize() const;
|
||||||
|
|
||||||
|
bool Seek(s64 offset, SeekOrigin origin);
|
||||||
|
|
||||||
|
// Returns 0 when not open.
|
||||||
|
u64 Tell() const { return m_current_offset; }
|
||||||
|
|
||||||
|
bool Flush();
|
||||||
|
|
||||||
|
auto GetHandle() const
|
||||||
|
{
|
||||||
|
#if defined(_WIN32)
|
||||||
|
return m_handle;
|
||||||
|
#else
|
||||||
|
return m_fd;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void Swap(DirectIOFile& other);
|
||||||
|
DirectIOFile Duplicate() const;
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
// A workaround to avoid including <windows.h> in this header.
|
||||||
|
// HANDLE is just void* and INVALID_HANDLE_VALUE is -1.
|
||||||
|
using HandleType = void*;
|
||||||
|
HandleType m_handle{HandleType(-1)};
|
||||||
|
#else
|
||||||
|
using HandleType = int;
|
||||||
|
HandleType m_fd{-1};
|
||||||
|
#endif
|
||||||
|
|
||||||
|
u64 m_current_offset{};
|
||||||
|
};
|
||||||
|
|
||||||
|
// These take an open file handle to avoid failures from other processes trying to open our files.
|
||||||
|
// This is mainly an issue on Windows.
|
||||||
|
|
||||||
|
bool Resize(DirectIOFile& file, u64 size);
|
||||||
|
|
||||||
|
// Attempts to replace a destination file if one already exists.
|
||||||
|
// Note: Windows uses `file`. Elsewhere uses `source_path`, but `file` is checked for openness.
|
||||||
|
bool Rename(DirectIOFile& file, const std::string& source_path,
|
||||||
|
const std::string& destination_path);
|
||||||
|
// Note: Ditto, only Windows actually uses the file handle. Provide both.
|
||||||
|
bool Delete(DirectIOFile& file, const std::string& filename);
|
||||||
|
|
||||||
|
} // namespace File
|
||||||
@ -51,6 +51,7 @@
|
|||||||
<ClInclude Include="Common\Debug\MemoryPatches.h" />
|
<ClInclude Include="Common\Debug\MemoryPatches.h" />
|
||||||
<ClInclude Include="Common\Debug\Threads.h" />
|
<ClInclude Include="Common\Debug\Threads.h" />
|
||||||
<ClInclude Include="Common\Debug\Watches.h" />
|
<ClInclude Include="Common\Debug\Watches.h" />
|
||||||
|
<ClInclude Include="Common\DirectIOFile.h" />
|
||||||
<ClInclude Include="Common\DynamicLibrary.h" />
|
<ClInclude Include="Common\DynamicLibrary.h" />
|
||||||
<ClInclude Include="Common\ENet.h" />
|
<ClInclude Include="Common\ENet.h" />
|
||||||
<ClInclude Include="Common\EnumFormatter.h" />
|
<ClInclude Include="Common\EnumFormatter.h" />
|
||||||
@ -820,6 +821,7 @@
|
|||||||
<ClCompile Include="Common\Crypto\SHA1.cpp" />
|
<ClCompile Include="Common\Crypto\SHA1.cpp" />
|
||||||
<ClCompile Include="Common\Debug\MemoryPatches.cpp" />
|
<ClCompile Include="Common\Debug\MemoryPatches.cpp" />
|
||||||
<ClCompile Include="Common\Debug\Watches.cpp" />
|
<ClCompile Include="Common\Debug\Watches.cpp" />
|
||||||
|
<ClCompile Include="Common\DirectIOFile.cpp" />
|
||||||
<ClCompile Include="Common\DynamicLibrary.cpp" />
|
<ClCompile Include="Common\DynamicLibrary.cpp" />
|
||||||
<ClCompile Include="Common\ENet.cpp" />
|
<ClCompile Include="Common\ENet.cpp" />
|
||||||
<ClCompile Include="Common\FatFsUtil.cpp" />
|
<ClCompile Include="Common\FatFsUtil.cpp" />
|
||||||
|
|||||||
@ -1,9 +1,15 @@
|
|||||||
// Copyright 2020 Dolphin Emulator Project
|
// Copyright 2020 Dolphin Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <latch>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include "Common/BitUtils.h"
|
||||||
|
#include "Common/DirectIOFile.h"
|
||||||
#include "Common/FileUtil.h"
|
#include "Common/FileUtil.h"
|
||||||
|
|
||||||
class FileUtilTest : public testing::Test
|
class FileUtilTest : public testing::Test
|
||||||
@ -147,3 +153,262 @@ TEST_F(FileUtilTest, CreateFullPath)
|
|||||||
EXPECT_FALSE(File::CreateFullPath(p3file + "/"));
|
EXPECT_FALSE(File::CreateFullPath(p3file + "/"));
|
||||||
EXPECT_TRUE(File::IsFile(p3file));
|
EXPECT_TRUE(File::IsFile(p3file));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(FileUtilTest, DirectIOFile)
|
||||||
|
{
|
||||||
|
static constexpr std::array<u8, 3> u8_test_data = {42, 7, 99};
|
||||||
|
static constexpr std::array<u16, 3> u16_test_data = {0xdead, 0xbeef, 0xf00d};
|
||||||
|
|
||||||
|
static constexpr int u16_data_offset = 73;
|
||||||
|
|
||||||
|
File::DirectIOFile file;
|
||||||
|
EXPECT_FALSE(file.IsOpen());
|
||||||
|
|
||||||
|
// Read mode fails with a non-existing file.
|
||||||
|
EXPECT_FALSE(file.Open(m_file_path, File::AccessMode::Read));
|
||||||
|
|
||||||
|
std::array<u8, u8_test_data.size()> u8_buffer = {};
|
||||||
|
|
||||||
|
// Everything fails when a file isn't open.
|
||||||
|
EXPECT_FALSE(file.Write(u8_buffer));
|
||||||
|
EXPECT_FALSE(file.Read(u8_buffer));
|
||||||
|
EXPECT_FALSE(file.Flush());
|
||||||
|
EXPECT_FALSE(file.Seek(12, File::SeekOrigin::Begin));
|
||||||
|
EXPECT_EQ(file.Tell(), 0);
|
||||||
|
EXPECT_EQ(file.GetSize(), 0);
|
||||||
|
|
||||||
|
// Fail to open non-existing file.
|
||||||
|
EXPECT_FALSE(file.Open(m_file_path, File::AccessMode::ReadAndWrite));
|
||||||
|
EXPECT_FALSE(file.Open(m_file_path, File::AccessMode::Read, File::OpenMode::Existing));
|
||||||
|
EXPECT_FALSE(file.Open(m_file_path, File::AccessMode::Write, File::OpenMode::Existing));
|
||||||
|
|
||||||
|
// Read mode can create files
|
||||||
|
EXPECT_TRUE(file.Open(m_file_path, File::AccessMode::Read, File::OpenMode::Create));
|
||||||
|
EXPECT_TRUE(file.Close());
|
||||||
|
|
||||||
|
// Open a file for writing.
|
||||||
|
EXPECT_TRUE(file.Open(m_file_path, File::AccessMode::Write, File::OpenMode::Always));
|
||||||
|
EXPECT_TRUE(file.Flush());
|
||||||
|
EXPECT_TRUE(file.IsOpen());
|
||||||
|
|
||||||
|
// Note: Double Open() currently ASSERTs. It's not obvious if that should succeed or fail.
|
||||||
|
|
||||||
|
EXPECT_TRUE(file.Close());
|
||||||
|
EXPECT_FALSE(file.Open(m_file_path, File::AccessMode::Write, File::OpenMode::Create));
|
||||||
|
|
||||||
|
EXPECT_TRUE(file.Open(m_file_path, File::AccessMode::Write, File::OpenMode::Existing));
|
||||||
|
EXPECT_TRUE(file.Close());
|
||||||
|
|
||||||
|
// Rename, Resize, and Delete fail with a closed handle.
|
||||||
|
EXPECT_FALSE(Rename(file, m_file_path, "fail"));
|
||||||
|
EXPECT_FALSE(Resize(file, 72));
|
||||||
|
EXPECT_FALSE(Delete(file, m_file_path));
|
||||||
|
EXPECT_TRUE(File::Exists(m_file_path));
|
||||||
|
|
||||||
|
EXPECT_TRUE(file.Open(m_file_path, File::AccessMode::Write));
|
||||||
|
|
||||||
|
EXPECT_TRUE(file.Write(u8_buffer.data(), 0)); // write of 0 succeeds.
|
||||||
|
EXPECT_FALSE(file.Read(u8_buffer.data(), 0)); // read of 0 (in write mode) fails.
|
||||||
|
|
||||||
|
// Resize through handle works.
|
||||||
|
EXPECT_TRUE(Resize(file, 1));
|
||||||
|
EXPECT_EQ(file.GetSize(), 1);
|
||||||
|
file.Close();
|
||||||
|
|
||||||
|
// Write truncates by default.
|
||||||
|
EXPECT_TRUE(file.Open(m_file_path, File::AccessMode::Write));
|
||||||
|
EXPECT_EQ(file.GetSize(), 0);
|
||||||
|
|
||||||
|
EXPECT_TRUE(file.Write(u8_test_data));
|
||||||
|
EXPECT_EQ(file.GetSize(), u8_test_data.size()); // size changed
|
||||||
|
EXPECT_EQ(file.Tell(), u8_test_data.size()); // file position changed
|
||||||
|
|
||||||
|
// Relative seek works.
|
||||||
|
EXPECT_TRUE(file.Seek(0, File::SeekOrigin::End));
|
||||||
|
EXPECT_EQ(file.Tell(), file.GetSize());
|
||||||
|
EXPECT_TRUE(file.Seek(-int(u8_test_data.size()), File::SeekOrigin::Current));
|
||||||
|
EXPECT_EQ(file.Tell(), 0);
|
||||||
|
|
||||||
|
// Read while in "write mode" fails
|
||||||
|
EXPECT_FALSE(file.Read(u8_buffer));
|
||||||
|
EXPECT_EQ(file.Tell(), 0);
|
||||||
|
|
||||||
|
// Seeking past the end works.
|
||||||
|
EXPECT_TRUE(file.Seek(u16_data_offset, File::SeekOrigin::Begin));
|
||||||
|
EXPECT_EQ(file.Tell(), u16_data_offset);
|
||||||
|
EXPECT_EQ(file.GetSize(), u8_test_data.size()); // no change in size
|
||||||
|
|
||||||
|
static constexpr u64 final_file_size = u16_data_offset + sizeof(u16_test_data);
|
||||||
|
|
||||||
|
// Size changes after write.
|
||||||
|
EXPECT_TRUE(file.Write(Common::AsU8Span(u16_test_data)));
|
||||||
|
EXPECT_EQ(file.GetSize(), final_file_size);
|
||||||
|
|
||||||
|
// Seek before begin fails.
|
||||||
|
EXPECT_FALSE(file.Seek(-1, File::SeekOrigin::Begin));
|
||||||
|
EXPECT_EQ(file.Tell(), file.GetSize()); // unchanged position
|
||||||
|
|
||||||
|
EXPECT_TRUE(file.Close());
|
||||||
|
EXPECT_FALSE(file.IsOpen());
|
||||||
|
|
||||||
|
EXPECT_EQ(file.Tell(), 0);
|
||||||
|
EXPECT_EQ(file.GetSize(), 0);
|
||||||
|
|
||||||
|
EXPECT_FALSE(file.Close()); // Double close fails.
|
||||||
|
|
||||||
|
// Open file for reading.
|
||||||
|
EXPECT_TRUE(file.Open(m_file_path, File::AccessMode::Read, File::OpenMode::Always));
|
||||||
|
EXPECT_TRUE(file.Close());
|
||||||
|
EXPECT_TRUE(file.Open(m_file_path, File::AccessMode::Read));
|
||||||
|
|
||||||
|
static constexpr int big_offset = 999;
|
||||||
|
|
||||||
|
// We can seek beyond the end.
|
||||||
|
EXPECT_TRUE(file.Seek(big_offset, File::SeekOrigin::End));
|
||||||
|
EXPECT_EQ(file.Tell(), file.GetSize() + big_offset);
|
||||||
|
|
||||||
|
// Resize through handle fails when in read mode.
|
||||||
|
EXPECT_FALSE(Resize(file, big_offset));
|
||||||
|
EXPECT_EQ(file.GetSize(), final_file_size);
|
||||||
|
|
||||||
|
constexpr size_t thread_count = 4;
|
||||||
|
|
||||||
|
File::DirectIOFile closed_file;
|
||||||
|
|
||||||
|
std::latch do_reads{1};
|
||||||
|
|
||||||
|
EXPECT_TRUE(file.Seek(u16_data_offset, File::SeekOrigin::Begin));
|
||||||
|
|
||||||
|
// Concurrent access with copied handles works.
|
||||||
|
std::vector<std::thread> threads(thread_count);
|
||||||
|
for (auto& t : threads)
|
||||||
|
{
|
||||||
|
t = std::thread{[&do_reads, file, closed_file]() mutable {
|
||||||
|
EXPECT_FALSE(closed_file.IsOpen()); // a copied closed file is still closed
|
||||||
|
|
||||||
|
EXPECT_EQ(file.Tell(), u16_data_offset); // current position is copied
|
||||||
|
|
||||||
|
std::array<u8, 1> one_byte{};
|
||||||
|
|
||||||
|
constexpr u64 small_offset = 3;
|
||||||
|
|
||||||
|
do_reads.wait();
|
||||||
|
|
||||||
|
EXPECT_TRUE(file.Seek(small_offset, File::SeekOrigin::Begin));
|
||||||
|
EXPECT_EQ(file.Tell(), small_offset);
|
||||||
|
|
||||||
|
// writes fail in read mode.
|
||||||
|
EXPECT_FALSE(file.Write(u8_test_data));
|
||||||
|
EXPECT_FALSE(file.OffsetWrite(0, u8_test_data.data(), 0));
|
||||||
|
EXPECT_EQ(file.Tell(), small_offset);
|
||||||
|
|
||||||
|
EXPECT_TRUE(file.Read(one_byte.data(), 0)); // read of zero succeeds.
|
||||||
|
EXPECT_EQ(file.Tell(), small_offset); // unchanged position
|
||||||
|
|
||||||
|
// Reading at an explicit offset doesn't change the position
|
||||||
|
EXPECT_TRUE(file.OffsetRead(u8_test_data.size() - 1, one_byte));
|
||||||
|
EXPECT_EQ(file.Tell(), small_offset);
|
||||||
|
EXPECT_EQ(one_byte[0], u8_test_data.back());
|
||||||
|
|
||||||
|
EXPECT_EQ(file.GetSize(), final_file_size);
|
||||||
|
EXPECT_TRUE(file.Seek(-int(sizeof(u16_test_data)), File::SeekOrigin::End));
|
||||||
|
|
||||||
|
// We can't read beyond the end of the file.
|
||||||
|
EXPECT_FALSE(file.OffsetRead(big_offset, one_byte));
|
||||||
|
// A read of zero beyond the end works.
|
||||||
|
EXPECT_TRUE(file.OffsetRead(big_offset, one_byte.data(), 0));
|
||||||
|
|
||||||
|
// Reading the previously written data works.
|
||||||
|
std::array<u16, u16_test_data.size()> u16_buffer = {};
|
||||||
|
EXPECT_TRUE(file.Read(Common::AsWritableU8Span(u16_buffer)));
|
||||||
|
EXPECT_TRUE(std::ranges::equal(u16_buffer, u16_test_data));
|
||||||
|
|
||||||
|
// We can't read at the end of the file.
|
||||||
|
EXPECT_FALSE(file.Read(one_byte));
|
||||||
|
EXPECT_EQ(file.Tell(), file.GetSize()); // The position is unchanged.
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
// We may close the file on our end before the other threads use it.
|
||||||
|
file.Close();
|
||||||
|
|
||||||
|
// ReadAndWrite does not truncate existing files.
|
||||||
|
EXPECT_TRUE(file.Open(m_file_path, File::AccessMode::ReadAndWrite));
|
||||||
|
EXPECT_EQ(file.GetSize(), final_file_size);
|
||||||
|
file.Close();
|
||||||
|
|
||||||
|
// Write doesn't truncate when the mode is changed.
|
||||||
|
EXPECT_TRUE(file.Open(m_file_path, File::AccessMode::Write, File::OpenMode::Existing));
|
||||||
|
EXPECT_EQ(file.GetSize(), final_file_size);
|
||||||
|
|
||||||
|
// Open a new handle to the same file.
|
||||||
|
File::DirectIOFile second_handle(m_file_path, File::AccessMode::Write, File::OpenMode::Always);
|
||||||
|
EXPECT_TRUE(second_handle.IsOpen());
|
||||||
|
|
||||||
|
const std::string destination_path_1 = m_file_path + ".dest_1";
|
||||||
|
|
||||||
|
// Rename through handle works when opened elsewhere.
|
||||||
|
EXPECT_TRUE(Rename(file, m_file_path, destination_path_1));
|
||||||
|
EXPECT_FALSE(File::Exists(m_file_path));
|
||||||
|
EXPECT_TRUE(File::Exists(destination_path_1));
|
||||||
|
|
||||||
|
const std::string destination_path_2 = m_file_path + ".dest_2";
|
||||||
|
|
||||||
|
// Note: Windows fails the next `Rename` if this file is kept open.
|
||||||
|
// I don't know if there is a nice way to make that work.
|
||||||
|
{
|
||||||
|
File::DirectIOFile another_file{destination_path_2, File::AccessMode::Write};
|
||||||
|
EXPECT_TRUE(another_file.IsOpen());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename overwrites existing files.
|
||||||
|
EXPECT_TRUE(Rename(file, destination_path_1, destination_path_2));
|
||||||
|
EXPECT_FALSE(File::Exists(destination_path_1));
|
||||||
|
EXPECT_EQ(File::GetSize(destination_path_2), final_file_size);
|
||||||
|
|
||||||
|
const std::string destination_path_3 = m_file_path + ".dest_3";
|
||||||
|
|
||||||
|
// Truncate fails in Read mode.
|
||||||
|
File::Copy(destination_path_2, destination_path_3);
|
||||||
|
EXPECT_EQ(File::GetSize(destination_path_3), final_file_size);
|
||||||
|
{
|
||||||
|
File::DirectIOFile tmp{destination_path_3, File::AccessMode::Read, File::OpenMode::Truncate};
|
||||||
|
EXPECT_FALSE(tmp.IsOpen());
|
||||||
|
EXPECT_EQ(File::GetSize(destination_path_3), final_file_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Truncate works with ReadAndWrite.
|
||||||
|
EXPECT_EQ(File::GetSize(destination_path_3), final_file_size);
|
||||||
|
{
|
||||||
|
File::DirectIOFile tmp{destination_path_3, File::AccessMode::ReadAndWrite,
|
||||||
|
File::OpenMode::Truncate};
|
||||||
|
EXPECT_EQ(tmp.GetSize(), 0);
|
||||||
|
Delete(tmp, destination_path_3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Truncate works with Write.
|
||||||
|
File::Copy(destination_path_2, destination_path_3);
|
||||||
|
EXPECT_EQ(File::GetSize(destination_path_3), final_file_size);
|
||||||
|
{
|
||||||
|
File::DirectIOFile tmp{destination_path_3, File::AccessMode::Write, File::OpenMode::Truncate};
|
||||||
|
EXPECT_EQ(tmp.GetSize(), 0);
|
||||||
|
Delete(tmp, destination_path_3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete through handle works.
|
||||||
|
EXPECT_TRUE(Delete(file, destination_path_2));
|
||||||
|
EXPECT_FALSE(File::Exists(destination_path_2));
|
||||||
|
|
||||||
|
// The threads can read even after deleting everything.
|
||||||
|
do_reads.count_down();
|
||||||
|
std::ranges::for_each(threads, &std::thread::join);
|
||||||
|
|
||||||
|
file.Close();
|
||||||
|
EXPECT_TRUE(file.Open(destination_path_1, File::AccessMode::Read, File::OpenMode::Always));
|
||||||
|
|
||||||
|
// Required on Windows for the below to succeed.
|
||||||
|
second_handle.Close();
|
||||||
|
|
||||||
|
file.Close();
|
||||||
|
EXPECT_TRUE(file.Open(destination_path_2, File::AccessMode::Write, File::OpenMode::Always));
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user