dolphin/Source/Core/Common/DirectIOFile.cpp
JosJuice 9883235c0c Common: Treat DirectIOFile as unable to create SAF files
In 405baed805, we made the assumption that whether OpenAndroidContent
is able to create a new file depends on the open mode, the document
provider, and the positions of the celestial bodies. However, I'm
fairly sure that it can't create files at all as currently implemented.

First, ContentResolver.openFileDescriptor is documented as throwing
FileNotFoundException if the file doesn't exist. Now, the SAF
documentation is notoriously unreliable on matters like these, and
document providers can do whatever they want anyway, so we can't
actually trust this to mean that FileNotFoundException will always
be thrown if the file doesn't exist. But second, the Dolphin function
ContentHandler.unmangle is also unable to handle files that don't
already exist (unless the passed-in URI isn't mangled to begin with,
but to the best of my knowledge, there's no way to get a non-mangled URI
for a file that doesn't exist (unless you make assumptions about how the
document provider's URI scheme works, which we don't do in Dolphin)).
Summed up, it looks a lot like OpenAndroidContent can't create files,
or at the very least can't do so reliably.

Therefore I'm making DirectIOFile throw an assertion and return false
in situations where it's being asked to create a file under SAF. For
reference, there's no code in Dolphin that actually tries to create a
file under SAF. In all instances where we use SAF to write to files, the
file is created by the system file picker before it returns the URI of
the file to us.

What does this change gain us? First, if someone in the future tries to
use DirectIOFile to create a file under SAF, they'll be immediately
informed that this isn't supported rather than discovering it doesn't
work and chalking it up to SAF being bad for unpredictable reasons.
Second, we get rid of a call to File::Exists, which is a notable
performance improvement for game list scanning due to SAF and Dolphin's
"unmangling" being bad for reasons that unfortunately are entirely
predictable to those of us who have stared into the SAF void.

I've tested that game list scanning and game conversion still works.
2025-11-18 23:17:03 +01:00

364 lines
9.2 KiB
C++

// 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";
if (open_mode == OpenMode::Truncate)
open_mode_str += 't';
if (open_mode == OpenMode::Create)
{
ASSERT_MSG(COMMON, false, "DirectIOFile doesn't support creating SAF files");
return false;
}
m_fd = OpenAndroidContent(path, open_mode_str);
if (!IsOpen() && (open_mode == OpenMode::Always || open_mode == OpenMode::Truncate))
ASSERT_MSG(COMMON, Exists(path), "DirectIOFile doesn't support creating SAF files");
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