mirror of
https://github.com/SpeedBreakerProject/speedbreaker.git
synced 2026-10-18 05:01:21 +02:00
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
685 lines
24 KiB
C++
685 lines
24 KiB
C++
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
|
|
//
|
|
// File system exports. Semantics, info classes and struct layouts follow
|
|
// Xenia's xboxkrnl_io.cc / xboxkrnl_io_info.cc / info/file.h / info/volume.h.
|
|
// All I/O completes synchronously (as in Xenia); for files opened for
|
|
// asynchronous I/O the call still reports STATUS_PENDING after filling in
|
|
// the status block and signalling the event.
|
|
#include <stdafx.h>
|
|
#include <cerrno>
|
|
#include <kernel/write_watch.h>
|
|
#include <report/report.h>
|
|
#include "dispatcher.h"
|
|
#include "function.h"
|
|
#include "vfs.h"
|
|
|
|
#include <fcntl.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
|
|
namespace
|
|
{
|
|
constexpr uint32_t STATUS_NO_MORE_FILES = 0x80000006;
|
|
constexpr uint32_t STATUS_BUFFER_OVERFLOW_ = 0x80000005;
|
|
constexpr uint32_t STATUS_INVALID_INFO_CLASS = 0xC0000003;
|
|
constexpr uint32_t STATUS_INFO_LENGTH_MISMATCH = 0xC0000004;
|
|
constexpr uint32_t STATUS_OBJECT_NAME_COLLISION = 0xC0000035;
|
|
constexpr uint32_t STATUS_FILE_IS_A_DIRECTORY = 0xC00000BA;
|
|
constexpr uint32_t STATUS_NOT_A_DIRECTORY = 0xC0000103;
|
|
|
|
// Creation dispositions.
|
|
enum : uint32_t { FILE_SUPERSEDE, FILE_OPEN, FILE_CREATE, FILE_OPEN_IF, FILE_OVERWRITE, FILE_OVERWRITE_IF };
|
|
// IO_STATUS_BLOCK.Information after a create.
|
|
enum : uint32_t { FILE_SUPERSEDED, FILE_OPENED, FILE_CREATED, FILE_OVERWRITTEN, FILE_EXISTS, FILE_DOES_NOT_EXIST };
|
|
// Create options.
|
|
constexpr uint32_t FILE_DIRECTORY_FILE = 0x01;
|
|
constexpr uint32_t FILE_SYNCHRONOUS_IO_ALERT = 0x10;
|
|
constexpr uint32_t FILE_SYNCHRONOUS_IO_NONALERT = 0x20;
|
|
constexpr uint32_t FILE_NON_DIRECTORY_FILE = 0x40;
|
|
// Access.
|
|
constexpr uint32_t GENERIC_WRITE_ = 0x40000000;
|
|
constexpr uint32_t FILE_WRITE_DATA = 0x0002;
|
|
constexpr uint32_t FILE_APPEND_DATA = 0x0004;
|
|
// Attributes.
|
|
constexpr uint32_t ATTR_READONLY = 0x01;
|
|
constexpr uint32_t ATTR_DIRECTORY = 0x10;
|
|
constexpr uint32_t ATTR_NORMAL = 0x80;
|
|
|
|
constexpr int64_t FILETIME_EPOCH_DIFFERENCE = 116444736000000000LL;
|
|
|
|
struct HostAttributes
|
|
{
|
|
uint64_t size = 0;
|
|
uint64_t writeTime = 0;
|
|
uint32_t attributes = 0;
|
|
};
|
|
|
|
std::optional<HostAttributes> Stat(const std::filesystem::path& path, bool readOnly)
|
|
{
|
|
struct stat st;
|
|
if (stat(path.c_str(), &st) != 0)
|
|
return std::nullopt;
|
|
HostAttributes a;
|
|
bool dir = S_ISDIR(st.st_mode);
|
|
a.size = dir ? 0 : uint64_t(st.st_size);
|
|
#ifdef __APPLE__
|
|
int64_t sec = st.st_mtimespec.tv_sec, nsec = st.st_mtimespec.tv_nsec;
|
|
#else
|
|
int64_t sec = st.st_mtim.tv_sec, nsec = st.st_mtim.tv_nsec;
|
|
#endif
|
|
a.writeTime = uint64_t(sec * 10000000 + nsec / 100 + FILETIME_EPOCH_DIFFERENCE);
|
|
a.attributes = dir ? ATTR_DIRECTORY : ATTR_NORMAL;
|
|
if (readOnly)
|
|
a.attributes |= ATTR_READONLY;
|
|
return a;
|
|
}
|
|
|
|
uint64_t AllocationSize(uint64_t size)
|
|
{
|
|
return (size + 0xFFF) & ~uint64_t(0xFFF);
|
|
}
|
|
}
|
|
|
|
struct X_FILE_NETWORK_OPEN_INFORMATION
|
|
{
|
|
be<uint64_t> creationTime;
|
|
be<uint64_t> lastAccessTime;
|
|
be<uint64_t> lastWriteTime;
|
|
be<uint64_t> changeTime;
|
|
be<uint64_t> allocationSize;
|
|
be<uint64_t> endOfFile;
|
|
be<uint32_t> attributes;
|
|
be<uint32_t> pad;
|
|
};
|
|
static_assert(sizeof(X_FILE_NETWORK_OPEN_INFORMATION) == 56);
|
|
|
|
struct X_FILE_DIRECTORY_INFORMATION
|
|
{
|
|
be<uint32_t> nextEntryOffset;
|
|
be<uint32_t> fileIndex;
|
|
be<uint64_t> creationTime;
|
|
be<uint64_t> lastAccessTime;
|
|
be<uint64_t> lastWriteTime;
|
|
be<uint64_t> changeTime;
|
|
be<uint64_t> endOfFile;
|
|
be<uint64_t> allocationSize;
|
|
be<uint32_t> attributes;
|
|
be<uint32_t> fileNameLength;
|
|
char fileName[1];
|
|
};
|
|
static_assert(offsetof(X_FILE_DIRECTORY_INFORMATION, fileName) == 0x40);
|
|
|
|
// Signaled at all times: every operation has completed by the time it
|
|
// returns, so waiting on the file handle never blocks.
|
|
struct FileObject final : Waitable
|
|
{
|
|
int fd = -1;
|
|
std::filesystem::path path;
|
|
std::string guestPath;
|
|
bool readOnly = false;
|
|
bool isDirectory = false;
|
|
bool synchronous = true;
|
|
uint64_t position = 0;
|
|
// Directory enumeration.
|
|
std::vector<std::filesystem::path> entries;
|
|
size_t nextEntry = 0;
|
|
std::string pattern;
|
|
|
|
~FileObject() override
|
|
{
|
|
if (fd >= 0)
|
|
close(fd);
|
|
}
|
|
|
|
bool IsSignaledLocked() const override { return true; }
|
|
|
|
void FillNetworkOpenInformation(X_FILE_NETWORK_OPEN_INFORMATION* info) const
|
|
{
|
|
auto a = Stat(path, readOnly).value_or(HostAttributes{});
|
|
info->creationTime = a.writeTime;
|
|
info->lastAccessTime = a.writeTime;
|
|
info->lastWriteTime = a.writeTime;
|
|
info->changeTime = a.writeTime;
|
|
info->allocationSize = AllocationSize(a.size);
|
|
info->endOfFile = a.size;
|
|
info->attributes = a.attributes;
|
|
info->pad = 0;
|
|
}
|
|
};
|
|
|
|
namespace
|
|
{
|
|
FileObject* FileFromHandle(uint32_t handle)
|
|
{
|
|
if (handle == GUEST_INVALID_HANDLE_VALUE || !IsKernelObject(handle))
|
|
return nullptr;
|
|
return dynamic_cast<FileObject*>(GetKernelObject(handle));
|
|
}
|
|
|
|
std::string GuestString(const XANSI_STRING* s)
|
|
{
|
|
if (s == nullptr || s->Buffer.get() == nullptr)
|
|
return {};
|
|
return std::string(s->Buffer.get(), uint16_t(s->Length));
|
|
}
|
|
|
|
std::optional<std::string> ObjectPath(const XOBJECT_ATTRIBUTES* attributes)
|
|
{
|
|
std::string name = GuestString(attributes->Name.get());
|
|
uint32_t root = attributes->RootDirectory;
|
|
if (root != 0 && root != 0xFFFFFFFD) // 0xFFFFFFFD = ObDosDevices
|
|
{
|
|
FileObject* dir = FileFromHandle(root);
|
|
if (dir == nullptr)
|
|
return std::nullopt;
|
|
return dir->guestPath + "\\" + name;
|
|
}
|
|
return name;
|
|
}
|
|
|
|
void Complete(XIO_STATUS_BLOCK* iosb, uint32_t status, uint32_t information)
|
|
{
|
|
if (iosb)
|
|
{
|
|
iosb->Status = status;
|
|
iosb->Information = information;
|
|
}
|
|
}
|
|
|
|
void SignalEvent(uint32_t eventHandle)
|
|
{
|
|
if (eventHandle == 0 || !IsKernelObject(eventHandle))
|
|
return;
|
|
if (auto* e = dynamic_cast<EventObject*>(GetKernelObject(eventHandle)))
|
|
e->Set();
|
|
}
|
|
|
|
void CheckNoApc(uint32_t apcRoutine, const char* who)
|
|
{
|
|
if (apcRoutine & ~1u)
|
|
{
|
|
fprintf(stderr, "[file] %s with an APC completion routine (%08X) is not supported yet\n", who, apcRoutine);
|
|
assert(false && "I/O APC routines are not implemented");
|
|
}
|
|
}
|
|
}
|
|
|
|
uint32_t NtCreateFile(be<uint32_t>* handleOut, uint32_t desiredAccess, XOBJECT_ATTRIBUTES* attributes,
|
|
XIO_STATUS_BLOCK* iosb, be<uint64_t>* allocationSize, uint32_t fileAttributes, uint32_t shareAccess,
|
|
uint32_t disposition, uint32_t createOptions)
|
|
{
|
|
if (attributes == nullptr || handleOut == nullptr)
|
|
return STATUS_INVALID_PARAMETER;
|
|
*handleOut = GUEST_INVALID_HANDLE_VALUE;
|
|
|
|
auto guestPath = ObjectPath(attributes);
|
|
if (!guestPath)
|
|
{
|
|
Complete(iosb, STATUS_INVALID_HANDLE, 0);
|
|
return STATUS_INVALID_HANDLE;
|
|
}
|
|
|
|
bool wantsWrite = (desiredAccess & (GENERIC_WRITE_ | FILE_WRITE_DATA | FILE_APPEND_DATA)) != 0;
|
|
bool mayCreate = disposition != FILE_OPEN && disposition != FILE_OVERWRITE;
|
|
auto resolved = vfs::Resolve(*guestPath, !mayCreate);
|
|
if (!resolved)
|
|
{
|
|
fprintf(stderr, "[file] open \"%s\": not found\n", guestPath->c_str());
|
|
Complete(iosb, STATUS_OBJECT_NAME_NOT_FOUND, FILE_DOES_NOT_EXIST);
|
|
return STATUS_OBJECT_NAME_NOT_FOUND;
|
|
}
|
|
|
|
std::error_code ec;
|
|
bool exists = std::filesystem::exists(resolved->host, ec);
|
|
bool isDir = exists && std::filesystem::is_directory(resolved->host, ec);
|
|
bool wantDir = (createOptions & FILE_DIRECTORY_FILE) != 0;
|
|
|
|
auto fail = [&](uint32_t status, uint32_t info) {
|
|
fprintf(stderr, "[file] open \"%s\" failed: %08X\n", guestPath->c_str(), status);
|
|
Complete(iosb, status, info);
|
|
return status;
|
|
};
|
|
|
|
if (exists && disposition == FILE_CREATE)
|
|
return fail(STATUS_OBJECT_NAME_COLLISION, FILE_EXISTS);
|
|
if (!exists && (disposition == FILE_OPEN || disposition == FILE_OVERWRITE))
|
|
return fail(STATUS_OBJECT_NAME_NOT_FOUND, FILE_DOES_NOT_EXIST);
|
|
if (isDir && (createOptions & FILE_NON_DIRECTORY_FILE))
|
|
return fail(STATUS_FILE_IS_A_DIRECTORY, 0);
|
|
if (exists && !isDir && wantDir)
|
|
return fail(STATUS_NOT_A_DIRECTORY, 0);
|
|
if (resolved->readOnly && (wantsWrite || !exists))
|
|
return fail(STATUS_ACCESS_DENIED, 0);
|
|
|
|
uint32_t action = FILE_OPENED;
|
|
auto* file = CreateKernelObject<FileObject>();
|
|
file->path = resolved->host;
|
|
file->guestPath = *guestPath;
|
|
file->readOnly = resolved->readOnly;
|
|
file->synchronous = (createOptions & (FILE_SYNCHRONOUS_IO_ALERT | FILE_SYNCHRONOUS_IO_NONALERT)) != 0;
|
|
|
|
if (wantDir || isDir)
|
|
{
|
|
if (!exists)
|
|
{
|
|
std::filesystem::create_directories(resolved->host, ec);
|
|
action = FILE_CREATED;
|
|
}
|
|
file->isDirectory = true;
|
|
}
|
|
else
|
|
{
|
|
int flags = wantsWrite ? O_RDWR : O_RDONLY;
|
|
if (!exists)
|
|
{
|
|
flags |= O_CREAT;
|
|
action = FILE_CREATED;
|
|
}
|
|
else if (disposition == FILE_SUPERSEDE || disposition == FILE_OVERWRITE || disposition == FILE_OVERWRITE_IF)
|
|
{
|
|
flags |= O_TRUNC | O_RDWR;
|
|
action = disposition == FILE_SUPERSEDE ? FILE_SUPERSEDED : FILE_OVERWRITTEN;
|
|
}
|
|
file->fd = open(resolved->host.c_str(), flags, 0644);
|
|
if (file->fd < 0)
|
|
{
|
|
DestroyKernelObject(file);
|
|
return fail(STATUS_ACCESS_DENIED, 0);
|
|
}
|
|
}
|
|
|
|
*handleOut = GetKernelHandle(file);
|
|
fprintf(stderr, "[file] open \"%s\" -> %s%s\n", guestPath->c_str(), resolved->host.c_str(),
|
|
action == FILE_CREATED ? " (created)" : "");
|
|
Complete(iosb, STATUS_SUCCESS, action);
|
|
return STATUS_SUCCESS;
|
|
}
|
|
|
|
uint32_t NtOpenFile(be<uint32_t>* handleOut, uint32_t desiredAccess, XOBJECT_ATTRIBUTES* attributes,
|
|
XIO_STATUS_BLOCK* iosb, uint32_t openOptions)
|
|
{
|
|
return NtCreateFile(handleOut, desiredAccess, attributes, iosb, nullptr, 0, 0, FILE_OPEN, openOptions);
|
|
}
|
|
|
|
uint32_t NtReadFile(uint32_t fileHandle, uint32_t eventHandle, uint32_t apcRoutine, uint32_t apcContext,
|
|
XIO_STATUS_BLOCK* iosb, void* buffer, uint32_t length, be<int64_t>* byteOffset)
|
|
{
|
|
FileObject* file = FileFromHandle(fileHandle);
|
|
if (file == nullptr || file->isDirectory)
|
|
{
|
|
Complete(iosb, STATUS_INVALID_HANDLE, 0);
|
|
return STATUS_INVALID_HANDLE;
|
|
}
|
|
CheckNoApc(apcRoutine, "NtReadFile");
|
|
|
|
// A byte offset of -2 (FILE_USE_FILE_POINTER_POSITION) or none: current position.
|
|
int64_t offset = byteOffset ? int64_t(*byteOffset) : -1;
|
|
uint64_t pos = offset >= 0 ? uint64_t(offset) : file->position;
|
|
writewatch::BeforeHostWrite(buffer, length); // the kernel can't fault on watched pages
|
|
// Another thread can protect a page of the buffer again between
|
|
// BeforeHostWrite and the read (a texture's write watch, the GPU guard):
|
|
// the kernel then stops short at that page, or fails with EFAULT, instead
|
|
// of faulting. Any shortfall before end of file is read again, through a
|
|
// bounce buffer if the kernel refuses the guest memory, and copied with
|
|
// ordinary stores, which fault and are handled like any CPU write. (The
|
|
// game treats a failed read as fatal: its error callback is null, a crash
|
|
// at lr 82621950.)
|
|
uint8_t* dst = static_cast<uint8_t*>(buffer);
|
|
ssize_t n = 0;
|
|
while (uint32_t(n) < length)
|
|
{
|
|
ssize_t got = pread(file->fd, dst + n, length - uint32_t(n), off_t(pos + uint64_t(n)));
|
|
if (got < 0 && errno == EFAULT)
|
|
{
|
|
static thread_local std::vector<uint8_t> bounce;
|
|
bounce.resize(length - uint32_t(n));
|
|
do
|
|
got = pread(file->fd, bounce.data(), bounce.size(), off_t(pos + uint64_t(n)));
|
|
while (got < 0 && errno == EINTR);
|
|
if (got > 0)
|
|
memcpy(dst + n, bounce.data(), size_t(got));
|
|
}
|
|
if (got < 0 && errno == EINTR)
|
|
continue;
|
|
if (got < 0)
|
|
{
|
|
fprintf(stderr, "[file] read of \"%s\" failed: %s (offset %llu, %u bytes)\n", file->guestPath.c_str(),
|
|
strerror(errno), (unsigned long long)(pos + uint64_t(n)), length - uint32_t(n));
|
|
Complete(iosb, STATUS_UNSUCCESSFUL, 0);
|
|
return STATUS_UNSUCCESSFUL;
|
|
}
|
|
if (got == 0)
|
|
break; // end of file
|
|
n += got;
|
|
}
|
|
file->position = pos + uint64_t(n);
|
|
report::g_fileBytesRead.fetch_add(uint64_t(n), std::memory_order_relaxed); // the hang watchdog's "loading"
|
|
|
|
uint32_t status = (n == 0 && length != 0) ? STATUS_END_OF_FILE : STATUS_SUCCESS;
|
|
Complete(iosb, status, uint32_t(n));
|
|
SignalEvent(eventHandle);
|
|
if (status == STATUS_SUCCESS && !file->synchronous)
|
|
return STATUS_PENDING;
|
|
return status;
|
|
}
|
|
|
|
uint32_t NtWriteFile(uint32_t fileHandle, uint32_t eventHandle, uint32_t apcRoutine, uint32_t apcContext,
|
|
XIO_STATUS_BLOCK* iosb, const void* buffer, uint32_t length, be<int64_t>* byteOffset)
|
|
{
|
|
FileObject* file = FileFromHandle(fileHandle);
|
|
if (file == nullptr || file->isDirectory)
|
|
{
|
|
Complete(iosb, STATUS_INVALID_HANDLE, 0);
|
|
return STATUS_INVALID_HANDLE;
|
|
}
|
|
if (file->readOnly)
|
|
{
|
|
Complete(iosb, STATUS_ACCESS_DENIED, 0);
|
|
return STATUS_ACCESS_DENIED;
|
|
}
|
|
CheckNoApc(apcRoutine, "NtWriteFile");
|
|
|
|
int64_t offset = byteOffset ? int64_t(*byteOffset) : -1;
|
|
uint64_t pos = offset >= 0 ? uint64_t(offset) : file->position;
|
|
ssize_t n = pwrite(file->fd, buffer, length, off_t(pos));
|
|
if (n < 0)
|
|
{
|
|
Complete(iosb, STATUS_UNSUCCESSFUL, 0);
|
|
return STATUS_UNSUCCESSFUL;
|
|
}
|
|
file->position = pos + uint64_t(n);
|
|
Complete(iosb, STATUS_SUCCESS, uint32_t(n));
|
|
SignalEvent(eventHandle);
|
|
return file->synchronous ? STATUS_SUCCESS : STATUS_PENDING;
|
|
}
|
|
|
|
uint32_t NtFlushBuffersFile(uint32_t fileHandle, XIO_STATUS_BLOCK* iosb)
|
|
{
|
|
if (FileObject* file = FileFromHandle(fileHandle); file && file->fd >= 0 && !file->readOnly)
|
|
fsync(file->fd);
|
|
Complete(iosb, STATUS_SUCCESS, 0);
|
|
return STATUS_SUCCESS;
|
|
}
|
|
|
|
// Info classes (X_FILE_INFORMATION_CLASS).
|
|
enum : uint32_t
|
|
{
|
|
XFileBasicInformation = 4,
|
|
XFileStandardInformation = 5,
|
|
XFileInternalInformation = 6,
|
|
XFileDispositionInformation = 13,
|
|
XFilePositionInformation = 14,
|
|
XFileModeInformation = 16,
|
|
XFileAlignmentInformation = 17,
|
|
XFileAllocationInformation = 19,
|
|
XFileEndOfFileInformation = 20,
|
|
XFileSectorInformation = 26,
|
|
XFileIoPriorityInformation = 32,
|
|
XFileNetworkOpenInformation = 34,
|
|
};
|
|
|
|
uint32_t NtQueryInformationFile(uint32_t fileHandle, XIO_STATUS_BLOCK* iosb, uint8_t* info, uint32_t length,
|
|
uint32_t infoClass)
|
|
{
|
|
FileObject* file = FileFromHandle(fileHandle);
|
|
if (file == nullptr)
|
|
return STATUS_INVALID_HANDLE;
|
|
|
|
uint32_t written = 0;
|
|
switch (infoClass)
|
|
{
|
|
case XFileInternalInformation:
|
|
{
|
|
if (length < 8) return STATUS_INFO_LENGTH_MISMATCH;
|
|
*reinterpret_cast<be<uint64_t>*>(info) = std::hash<std::string>{}(file->path.string());
|
|
written = 8;
|
|
break;
|
|
}
|
|
case XFilePositionInformation:
|
|
{
|
|
if (length < 8) return STATUS_INFO_LENGTH_MISMATCH;
|
|
*reinterpret_cast<be<uint64_t>*>(info) = file->position;
|
|
written = 8;
|
|
break;
|
|
}
|
|
case XFileNetworkOpenInformation:
|
|
{
|
|
if (length < sizeof(X_FILE_NETWORK_OPEN_INFORMATION)) return STATUS_INFO_LENGTH_MISMATCH;
|
|
file->FillNetworkOpenInformation(reinterpret_cast<X_FILE_NETWORK_OPEN_INFORMATION*>(info));
|
|
written = sizeof(X_FILE_NETWORK_OPEN_INFORMATION);
|
|
break;
|
|
}
|
|
case XFileStandardInformation:
|
|
{
|
|
// AllocationSize, EndOfFile (8 each), NumberOfLinks (4), DeletePending, Directory.
|
|
if (length < 22) return STATUS_INFO_LENGTH_MISMATCH;
|
|
memset(info, 0, 24 <= length ? 24 : length);
|
|
auto a = Stat(file->path, file->readOnly).value_or(HostAttributes{});
|
|
reinterpret_cast<be<uint64_t>*>(info)[0] = AllocationSize(a.size);
|
|
reinterpret_cast<be<uint64_t>*>(info)[1] = a.size;
|
|
*reinterpret_cast<be<uint32_t>*>(info + 16) = 1;
|
|
info[20] = 0;
|
|
info[21] = file->isDirectory ? 1 : 0;
|
|
written = 24;
|
|
break;
|
|
}
|
|
case XFileAlignmentInformation:
|
|
case XFileModeInformation:
|
|
{
|
|
if (length < 4) return STATUS_INFO_LENGTH_MISMATCH;
|
|
*reinterpret_cast<be<uint32_t>*>(info) = 0;
|
|
written = 4;
|
|
break;
|
|
}
|
|
default:
|
|
fprintf(stderr, "[file] NtQueryInformationFile: info class %u not supported yet\n", infoClass);
|
|
assert(false && "unsupported NtQueryInformationFile class");
|
|
return STATUS_INVALID_INFO_CLASS;
|
|
}
|
|
Complete(iosb, STATUS_SUCCESS, written);
|
|
return STATUS_SUCCESS;
|
|
}
|
|
|
|
uint32_t NtSetInformationFile(uint32_t fileHandle, XIO_STATUS_BLOCK* iosb, uint8_t* info, uint32_t length,
|
|
uint32_t infoClass)
|
|
{
|
|
FileObject* file = FileFromHandle(fileHandle);
|
|
if (file == nullptr)
|
|
return STATUS_INVALID_HANDLE;
|
|
|
|
uint32_t status = STATUS_SUCCESS, written = 0;
|
|
switch (infoClass)
|
|
{
|
|
case XFilePositionInformation:
|
|
if (length < 8) return STATUS_INFO_LENGTH_MISMATCH;
|
|
file->position = *reinterpret_cast<be<uint64_t>*>(info);
|
|
written = 8;
|
|
break;
|
|
case XFileEndOfFileInformation:
|
|
if (length < 8) return STATUS_INFO_LENGTH_MISMATCH;
|
|
if (file->readOnly || file->fd < 0)
|
|
status = STATUS_ACCESS_DENIED;
|
|
else if (ftruncate(file->fd, off_t(uint64_t(*reinterpret_cast<be<uint64_t>*>(info)))) != 0)
|
|
status = STATUS_UNSUCCESSFUL;
|
|
written = 8;
|
|
break;
|
|
case XFileAllocationInformation:
|
|
if (length < 8) return STATUS_INFO_LENGTH_MISMATCH;
|
|
written = 8; // preallocation hint: ignored (as Xenia)
|
|
break;
|
|
case XFileDispositionInformation:
|
|
if (length < 1) return STATUS_INFO_LENGTH_MISMATCH;
|
|
if (info[0] != 0)
|
|
{
|
|
// Delete-on-close. Xenia ignores this; log it so we notice.
|
|
fprintf(stderr, "[file] delete-on-close requested for \"%s\" (ignored)\n", file->guestPath.c_str());
|
|
}
|
|
break;
|
|
default:
|
|
fprintf(stderr, "[file] NtSetInformationFile: info class %u not supported yet\n", infoClass);
|
|
assert(false && "unsupported NtSetInformationFile class");
|
|
return STATUS_INVALID_INFO_CLASS;
|
|
}
|
|
Complete(iosb, status, written);
|
|
return status;
|
|
}
|
|
|
|
uint32_t NtQueryFullAttributesFile(XOBJECT_ATTRIBUTES* attributes, X_FILE_NETWORK_OPEN_INFORMATION* info)
|
|
{
|
|
auto guestPath = ObjectPath(attributes);
|
|
if (!guestPath)
|
|
return STATUS_INVALID_HANDLE;
|
|
auto resolved = vfs::Resolve(*guestPath, true);
|
|
if (!resolved)
|
|
return STATUS_NO_SUCH_FILE;
|
|
auto a = Stat(resolved->host, resolved->readOnly);
|
|
if (!a)
|
|
return STATUS_NO_SUCH_FILE;
|
|
info->creationTime = a->writeTime;
|
|
info->lastAccessTime = a->writeTime;
|
|
info->lastWriteTime = a->writeTime;
|
|
info->changeTime = a->writeTime;
|
|
info->allocationSize = AllocationSize(a->size);
|
|
info->endOfFile = a->size;
|
|
info->attributes = a->attributes;
|
|
info->pad = 0;
|
|
return STATUS_SUCCESS;
|
|
}
|
|
|
|
uint32_t NtQueryDirectoryFile(uint32_t fileHandle, uint32_t eventHandle, uint32_t apcRoutine, uint32_t apcContext,
|
|
XIO_STATUS_BLOCK* iosb, X_FILE_DIRECTORY_INFORMATION* info, uint32_t length, XANSI_STRING* fileName,
|
|
uint32_t restartScan)
|
|
{
|
|
if (length < 72)
|
|
return STATUS_INFO_LENGTH_MISMATCH;
|
|
FileObject* dir = FileFromHandle(fileHandle);
|
|
if (dir == nullptr || !dir->isDirectory)
|
|
{
|
|
Complete(iosb, STATUS_NO_SUCH_FILE, 0);
|
|
return STATUS_NO_SUCH_FILE;
|
|
}
|
|
CheckNoApc(apcRoutine, "NtQueryDirectoryFile");
|
|
|
|
std::string pattern = GuestString(fileName);
|
|
// A new pattern restarts the scan (Xenia always restarts with a name).
|
|
if (!pattern.empty() || restartScan || dir->entries.empty())
|
|
{
|
|
if (!pattern.empty())
|
|
dir->pattern = pattern;
|
|
if (dir->pattern.empty())
|
|
dir->pattern = "*";
|
|
dir->entries.clear();
|
|
std::error_code ec;
|
|
for (auto& e : std::filesystem::directory_iterator(dir->path, ec))
|
|
if (vfs::WildcardMatch(dir->pattern, e.path().filename().string()))
|
|
dir->entries.push_back(e.path());
|
|
std::sort(dir->entries.begin(), dir->entries.end());
|
|
dir->nextEntry = 0;
|
|
}
|
|
|
|
if (dir->nextEntry >= dir->entries.size())
|
|
{
|
|
uint32_t status = dir->entries.empty() && !pattern.empty() ? STATUS_NO_SUCH_FILE : STATUS_NO_MORE_FILES;
|
|
Complete(iosb, status, 0);
|
|
return status;
|
|
}
|
|
|
|
const auto& entry = dir->entries[dir->nextEntry];
|
|
std::string name = entry.filename().string();
|
|
if (offsetof(X_FILE_DIRECTORY_INFORMATION, fileName) + name.size() > length)
|
|
{
|
|
Complete(iosb, STATUS_BUFFER_OVERFLOW_, 0);
|
|
return STATUS_BUFFER_OVERFLOW_;
|
|
}
|
|
dir->nextEntry++;
|
|
|
|
auto a = Stat(entry, dir->readOnly).value_or(HostAttributes{});
|
|
info->nextEntryOffset = 0;
|
|
info->fileIndex = uint32_t(dir->nextEntry);
|
|
info->creationTime = a.writeTime;
|
|
info->lastAccessTime = a.writeTime;
|
|
info->lastWriteTime = a.writeTime;
|
|
info->changeTime = a.writeTime;
|
|
info->endOfFile = a.size;
|
|
info->allocationSize = AllocationSize(a.size);
|
|
info->attributes = a.attributes;
|
|
info->fileNameLength = uint32_t(name.size());
|
|
memcpy(info->fileName, name.data(), name.size());
|
|
|
|
Complete(iosb, STATUS_SUCCESS, length);
|
|
SignalEvent(eventHandle);
|
|
return STATUS_SUCCESS;
|
|
}
|
|
|
|
// Volume info classes.
|
|
enum : uint32_t { XFileFsVolumeInformation = 1, XFileFsSizeInformation = 3, XFileFsAttributeInformation = 5 };
|
|
|
|
uint32_t NtQueryVolumeInformationFile(uint32_t fileHandle, XIO_STATUS_BLOCK* iosb, uint8_t* info, uint32_t length,
|
|
uint32_t infoClass)
|
|
{
|
|
FileObject* file = FileFromHandle(fileHandle);
|
|
if (file == nullptr)
|
|
return STATUS_INVALID_HANDLE;
|
|
memset(info, 0, length);
|
|
|
|
uint32_t status = STATUS_SUCCESS, written = 0;
|
|
switch (infoClass)
|
|
{
|
|
case XFileFsVolumeInformation:
|
|
// creation time (8), serial (4), label length (4), supports objects (1).
|
|
if (length < 24) return STATUS_INFO_LENGTH_MISMATCH;
|
|
written = 17;
|
|
break;
|
|
case XFileFsSizeInformation:
|
|
{
|
|
if (length < 24) return STATUS_INFO_LENGTH_MISMATCH;
|
|
// Report a 4 GB volume, mostly free, 16 KB clusters of 512-byte sectors
|
|
// (Xenia asserts bytes_per_sector == 0x200 for XCTD userland code).
|
|
auto* p = reinterpret_cast<be<uint64_t>*>(info);
|
|
p[0] = 0x40000; // total allocation units
|
|
p[1] = 0x38000; // available
|
|
reinterpret_cast<be<uint32_t>*>(info)[4] = 32; // sectors per unit
|
|
reinterpret_cast<be<uint32_t>*>(info)[5] = 0x200; // bytes per sector
|
|
written = 24;
|
|
break;
|
|
}
|
|
case XFileFsAttributeInformation:
|
|
{
|
|
if (length < 12) return STATUS_INFO_LENGTH_MISMATCH;
|
|
static constexpr char name[] = "FATX";
|
|
auto* p = reinterpret_cast<be<uint32_t>*>(info);
|
|
p[0] = 0; // attributes
|
|
p[1] = 255; // max component name length
|
|
p[2] = 4;
|
|
if (length >= 12 + 4)
|
|
{
|
|
memcpy(info + 12, name, 4);
|
|
written = 16;
|
|
}
|
|
else
|
|
{
|
|
status = STATUS_BUFFER_OVERFLOW_;
|
|
written = 12;
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
fprintf(stderr, "[file] NtQueryVolumeInformationFile: info class %u not supported yet\n", infoClass);
|
|
assert(false && "unsupported NtQueryVolumeInformationFile class");
|
|
return STATUS_INVALID_INFO_CLASS;
|
|
}
|
|
Complete(iosb, status, written);
|
|
return status;
|
|
}
|
|
|
|
GUEST_FUNCTION_HOOK(__imp__NtCreateFile, NtCreateFile);
|
|
GUEST_FUNCTION_HOOK(__imp__NtOpenFile, NtOpenFile);
|
|
GUEST_FUNCTION_HOOK(__imp__NtReadFile, NtReadFile);
|
|
GUEST_FUNCTION_HOOK(__imp__NtWriteFile, NtWriteFile);
|
|
GUEST_FUNCTION_HOOK(__imp__NtFlushBuffersFile, NtFlushBuffersFile);
|
|
GUEST_FUNCTION_HOOK(__imp__NtQueryInformationFile, NtQueryInformationFile);
|
|
GUEST_FUNCTION_HOOK(__imp__NtSetInformationFile, NtSetInformationFile);
|
|
GUEST_FUNCTION_HOOK(__imp__NtQueryFullAttributesFile, NtQueryFullAttributesFile);
|
|
GUEST_FUNCTION_HOOK(__imp__NtQueryDirectoryFile, NtQueryDirectoryFile);
|
|
GUEST_FUNCTION_HOOK(__imp__NtQueryVolumeInformationFile, NtQueryVolumeInformationFile);
|