/** ****************************************************************************** * Xenia : Xbox 360 Emulator Research Project * ****************************************************************************** * Copyright 2020 Ben Vanik. All rights reserved. * * Released under the BSD license - see LICENSE in the root for more details. * ****************************************************************************** */ #include "xenia/base/logging.h" #include "xenia/base/memory.h" #include "xenia/base/mutex.h" #include "xenia/cpu/processor.h" #include "xenia/kernel/info/file.h" #include "xenia/kernel/kernel_state.h" #include "xenia/kernel/util/shim_utils.h" #include "xenia/kernel/xboxkrnl/xboxkrnl_private.h" #include "xenia/kernel/xevent.h" #include "xenia/kernel/xfile.h" #include "xenia/kernel/xiocompletion.h" #include "xenia/kernel/xsymboliclink.h" #include "xenia/kernel/xthread.h" #include "xenia/vfs/device.h" #include "xenia/xbox.h" namespace xe { namespace kernel { namespace xboxkrnl { struct CreateOptions { // https://processhacker.sourceforge.io/doc/ntioapi_8h.html static const uint32_t FILE_DIRECTORY_FILE = 0x00000001; // Optimization - files access will be sequential, not random. static const uint32_t FILE_SEQUENTIAL_ONLY = 0x00000004; static const uint32_t FILE_SYNCHRONOUS_IO_ALERT = 0x00000010; static const uint32_t FILE_SYNCHRONOUS_IO_NONALERT = 0x00000020; static const uint32_t FILE_NON_DIRECTORY_FILE = 0x00000040; // Optimization - file access will be random, not sequential. static const uint32_t FILE_RANDOM_ACCESS = 0x00000800; }; static bool IsValidPath(const std::string_view s, bool is_pattern) { // TODO(gibbed): validate path components individually bool got_asterisk = false; for (const auto& c : s) { if (c <= 31 || c >= 127) { return false; } if (got_asterisk) { // * must be followed by a . (*.) // // Viva Piñata: Party Animals (4D530819) has a bug in its game code where // it attempts to FindFirstFile() with filters of "Game:\\*_X3.rkv", // "Game:\\m*_X3.rkv", and "Game:\\w*_X3.rkv" and will infinite loop if // the path filter is allowed. if (c != '.') { return false; } got_asterisk = false; } switch (c) { case '"': // case '*': case '+': case ',': // case ':': case ';': case '<': case '=': case '>': // case '?': case '|': { return false; } case '*': { // Pattern-specific (for NtQueryDirectoryFile) if (!is_pattern) { return false; } got_asterisk = true; break; } case '?': { // Pattern-specific (for NtQueryDirectoryFile) if (!is_pattern) { return false; } break; } default: { break; } } } return true; } dword_result_t NtCreateFile(lpdword_t handle_out, dword_t desired_access, pointer_t object_attrs, pointer_t io_status_block, lpqword_t allocation_size_ptr, dword_t file_attributes, dword_t share_access, dword_t creation_disposition, dword_t create_options) { uint64_t allocation_size = 0; // is this correct??? if (allocation_size_ptr) { allocation_size = *allocation_size_ptr; } if (!object_attrs) { // ..? Some games do this. This parameter is not optional. return X_STATUS_INVALID_PARAMETER; } assert_not_null(handle_out); auto object_name = kernel_memory()->TranslateVirtual(object_attrs->name_ptr); vfs::Entry* root_entry = nullptr; // Compute path, possibly attrs relative. auto target_path = util::TranslateAnsiString(kernel_memory(), object_name); // Enforce that the path is ASCII. if (!IsValidPath(target_path, false)) { return X_STATUS_OBJECT_NAME_INVALID; } if (object_attrs->root_directory != 0xFFFFFFFD && // ObDosDevices object_attrs->root_directory != 0) { auto root_file = kernel_state()->object_table()->LookupObject( object_attrs->root_directory); assert_not_null(root_file); assert_true(root_file->type() == XObject::Type::File); root_entry = root_file->entry(); } // Attempt open (or create). vfs::File* vfs_file; vfs::FileAction file_action; X_STATUS result = kernel_state()->file_system()->OpenFile( root_entry, target_path, vfs::FileDisposition((uint32_t)creation_disposition), desired_access, (create_options & CreateOptions::FILE_DIRECTORY_FILE) != 0, (create_options & CreateOptions::FILE_NON_DIRECTORY_FILE) != 0, &vfs_file, &file_action); object_ref file = nullptr; X_HANDLE handle = X_INVALID_HANDLE_VALUE; if (XSUCCEEDED(result)) { // If true, desired_access SYNCHRONIZE flag must be set. bool synchronous = (create_options & CreateOptions::FILE_SYNCHRONOUS_IO_ALERT) || (create_options & CreateOptions::FILE_SYNCHRONOUS_IO_NONALERT); file = object_ref(new XFile(kernel_state(), vfs_file, synchronous)); // Handle ref is incremented, so return that. handle = file->handle(); } if (io_status_block) { io_status_block->status = result; io_status_block->information = (uint32_t)file_action; } *handle_out = handle; return result; } DECLARE_XBOXKRNL_EXPORT1(NtCreateFile, kFileSystem, kImplemented); dword_result_t NtOpenFile(lpdword_t handle_out, dword_t desired_access, pointer_t object_attributes, pointer_t io_status_block, dword_t open_options) { return NtCreateFile(handle_out, desired_access, object_attributes, io_status_block, nullptr, 0, 0, static_cast(xe::vfs::FileDisposition::kOpen), open_options); } DECLARE_XBOXKRNL_EXPORT1(NtOpenFile, kFileSystem, kImplemented); dword_result_t NtReadFile(dword_t file_handle, dword_t event_handle, lpvoid_t apc_routine_ptr, lpvoid_t apc_context, pointer_t io_status_block, lpvoid_t buffer, dword_t buffer_length, lpqword_t byte_offset_ptr) { X_STATUS result = X_STATUS_SUCCESS; bool signal_event = false; auto ev = kernel_state()->object_table()->LookupObject(event_handle); if (event_handle && !ev) { result = X_STATUS_INVALID_HANDLE; } auto file = kernel_state()->object_table()->LookupObject(file_handle); if (!file) { result = X_STATUS_INVALID_HANDLE; } if (XSUCCEEDED(result)) { if (true || file->is_synchronous()) { // Synchronous. uint32_t bytes_read = 0; result = file->Read( buffer.guest_address(), buffer_length, byte_offset_ptr ? static_cast(*byte_offset_ptr) : -1, &bytes_read, apc_context); if (io_status_block) { io_status_block->status = result; io_status_block->information = bytes_read; } // Queue the APC callback. It must be delivered via the APC mechanism even // though were are completing immediately. // Low bit probably means do not queue to IO ports. if ((uint32_t)apc_routine_ptr & ~1) { if (apc_context) { auto thread = XThread::GetCurrentThread(); thread->EnqueueApc(static_cast(apc_routine_ptr) & ~1u, apc_context, io_status_block, 0); } } if (!file->is_synchronous()) { result = X_STATUS_PENDING; } // Mark that we should signal the event now. We do this after // we have written the info out. signal_event = true; } else { // TODO(benvanik): async. // X_STATUS_PENDING if not returning immediately. // XFile is waitable and signalled after each async req completes. // reset the input event (->Reset()) /*xeNtReadFileState* call_state = new xeNtReadFileState(); XAsyncRequest* request = new XAsyncRequest( state, file, (XAsyncRequest::CompletionCallback)xeNtReadFileCompleted, call_state);*/ // result = file->Read(buffer.guest_address(), buffer_length, byte_offset, // request); if (io_status_block) { io_status_block->status = X_STATUS_PENDING; io_status_block->information = 0; } result = X_STATUS_PENDING; } } if (XFAILED(result) && io_status_block) { io_status_block->status = result; io_status_block->information = 0; } if (ev && signal_event) { ev->Set(0, false); } return result; } DECLARE_XBOXKRNL_EXPORT2(NtReadFile, kFileSystem, kImplemented, kHighFrequency); dword_result_t NtWriteFile(dword_t file_handle, dword_t event_handle, function_t apc_routine, lpvoid_t apc_context, pointer_t io_status_block, lpvoid_t buffer, dword_t buffer_length, lpqword_t byte_offset_ptr) { // Async not supported yet. assert_zero(apc_routine); X_STATUS result = X_STATUS_SUCCESS; uint32_t info = 0; // Grab event to signal. bool signal_event = false; auto ev = kernel_state()->object_table()->LookupObject(event_handle); if (event_handle && !ev) { result = X_STATUS_INVALID_HANDLE; } // Grab file. auto file = kernel_state()->object_table()->LookupObject(file_handle); if (!file) { result = X_STATUS_INVALID_HANDLE; } // Execute write. if (XSUCCEEDED(result)) { // TODO(benvanik): async path. if (true || file->is_synchronous()) { // Synchronous request. uint32_t bytes_written = 0; result = file->Write( buffer.guest_address(), buffer_length, byte_offset_ptr ? static_cast(*byte_offset_ptr) : -1, &bytes_written, apc_context); if (XSUCCEEDED(result)) { info = bytes_written; } if (!file->is_synchronous()) { result = X_STATUS_PENDING; } if (io_status_block) { io_status_block->status = X_STATUS_SUCCESS; io_status_block->information = info; } // Mark that we should signal the event now. We do this after // we have written the info out. signal_event = true; } else { // X_STATUS_PENDING if not returning immediately. result = X_STATUS_PENDING; info = 0; if (io_status_block) { io_status_block->status = X_STATUS_SUCCESS; io_status_block->information = info; } } } if (XFAILED(result) && io_status_block) { io_status_block->status = result; io_status_block->information = 0; } if (ev && signal_event) { ev->Set(0, false); } return result; } DECLARE_XBOXKRNL_EXPORT1(NtWriteFile, kFileSystem, kImplemented); dword_result_t NtCreateIoCompletion(lpdword_t out_handle, dword_t desired_access, lpvoid_t object_attribs, dword_t num_concurrent_threads) { auto completion = new XIOCompletion(kernel_state()); if (out_handle) { *out_handle = completion->handle(); } return X_STATUS_SUCCESS; } DECLARE_XBOXKRNL_EXPORT1(NtCreateIoCompletion, kFileSystem, kImplemented); dword_result_t NtSetIoCompletion(dword_t handle, dword_t key_context, dword_t apc_context, dword_t completion_status, dword_t num_bytes) { auto port = kernel_state()->object_table()->LookupObject(handle); if (!port) { return X_STATUS_INVALID_HANDLE; } XIOCompletion::IONotification notification; notification.key_context = key_context; notification.apc_context = apc_context; notification.num_bytes = num_bytes; notification.status = completion_status; port->QueueNotification(notification); return X_STATUS_SUCCESS; } DECLARE_XBOXKRNL_EXPORT1(NtSetIoCompletion, kFileSystem, kImplemented); // Dequeues a packet from the completion port. dword_result_t NtRemoveIoCompletion( dword_t handle, lpdword_t key_context, lpdword_t apc_context, pointer_t io_status_block, lpqword_t timeout) { X_STATUS status = X_STATUS_SUCCESS; uint32_t info = 0; auto port = kernel_state()->object_table()->LookupObject(handle); if (!port) { status = X_STATUS_INVALID_HANDLE; } uint64_t timeout_ticks = timeout ? static_cast(*timeout) : 0u; XIOCompletion::IONotification notification; if (port->WaitForNotification(timeout_ticks, ¬ification)) { if (key_context) { *key_context = notification.key_context; } if (apc_context) { *apc_context = notification.apc_context; } if (io_status_block) { io_status_block->status = notification.status; io_status_block->information = notification.num_bytes; } } else { status = X_STATUS_TIMEOUT; } return status; } DECLARE_XBOXKRNL_EXPORT1(NtRemoveIoCompletion, kFileSystem, kImplemented); dword_result_t NtQueryFullAttributesFile( pointer_t obj_attribs, pointer_t file_info) { auto object_name = kernel_memory()->TranslateVirtual(obj_attribs->name_ptr); object_ref root_file; if (obj_attribs->root_directory != 0xFFFFFFFD && // ObDosDevices obj_attribs->root_directory != 0) { root_file = kernel_state()->object_table()->LookupObject( obj_attribs->root_directory); assert_not_null(root_file); assert_true(root_file->type() == XObject::Type::File); assert_always(); } auto target_path = util::TranslateAnsiString(kernel_memory(), object_name); // Enforce that the path is ASCII. if (!IsValidPath(target_path, false)) { return X_STATUS_OBJECT_NAME_INVALID; } // Resolve the file using the virtual file system. auto entry = kernel_state()->file_system()->ResolvePath(target_path); if (entry) { // Found. file_info->creation_time = entry->create_timestamp(); file_info->last_access_time = entry->access_timestamp(); file_info->last_write_time = entry->write_timestamp(); file_info->change_time = entry->write_timestamp(); file_info->allocation_size = entry->allocation_size(); file_info->end_of_file = entry->size(); file_info->attributes = entry->attributes(); return X_STATUS_SUCCESS; } return X_STATUS_NO_SUCH_FILE; } DECLARE_XBOXKRNL_EXPORT1(NtQueryFullAttributesFile, kFileSystem, kImplemented); dword_result_t NtQueryDirectoryFile( dword_t file_handle, dword_t event_handle, function_t apc_routine, lpvoid_t apc_context, pointer_t io_status_block, pointer_t file_info_ptr, dword_t length, pointer_t file_name, dword_t restart_scan) { if (length < 72) { return X_STATUS_INFO_LENGTH_MISMATCH; } X_STATUS result = X_STATUS_UNSUCCESSFUL; uint32_t info = 0; auto file = kernel_state()->object_table()->LookupObject(file_handle); auto name = util::TranslateAnsiString(kernel_memory(), file_name); // Enforce that the path is ASCII. if (!IsValidPath(name, true)) { return X_STATUS_INVALID_PARAMETER; } if (file) { X_FILE_DIRECTORY_INFORMATION dir_info = {0}; result = file->QueryDirectory(file_info_ptr, length, name, restart_scan != 0); if (XSUCCEEDED(result)) { info = length; } } else { result = X_STATUS_NO_SUCH_FILE; } if (XFAILED(result)) { info = 0; } if (io_status_block) { io_status_block->status = result; io_status_block->information = info; } return result; } DECLARE_XBOXKRNL_EXPORT1(NtQueryDirectoryFile, kFileSystem, kImplemented); dword_result_t NtFlushBuffersFile( dword_t file_handle, pointer_t io_status_block_ptr) { auto result = X_STATUS_SUCCESS; if (io_status_block_ptr) { io_status_block_ptr->status = result; io_status_block_ptr->information = 0; } return result; } DECLARE_XBOXKRNL_EXPORT1(NtFlushBuffersFile, kFileSystem, kStub); // https://docs.microsoft.com/en-us/windows/win32/devnotes/ntopensymboliclinkobject dword_result_t NtOpenSymbolicLinkObject( lpdword_t handle_out, pointer_t object_attrs) { if (!object_attrs) { return X_STATUS_INVALID_PARAMETER; } assert_not_null(handle_out); assert_true(object_attrs->attributes == 64); // case insensitive auto object_name = kernel_memory()->TranslateVirtual(object_attrs->name_ptr); std::string target_path = util::TranslateAnsiString(kernel_memory(), object_name); // Enforce that the path is ASCII. if (!IsValidPath(target_path, false)) { return X_STATUS_OBJECT_NAME_INVALID; } if (object_attrs->root_directory != 0) { assert_always(); } if (utf8::starts_with(target_path, "\\??\\")) { target_path = target_path.substr(4); // Strip the full qualifier } std::string link_path; if (!kernel_state()->file_system()->FindSymbolicLink(target_path, link_path)) { return X_STATUS_NO_SUCH_FILE; } object_ref symlink(new XSymbolicLink(kernel_state())); symlink->Initialize(target_path, link_path); *handle_out = symlink->handle(); return X_STATUS_SUCCESS; } DECLARE_XBOXKRNL_EXPORT1(NtOpenSymbolicLinkObject, kFileSystem, kImplemented); // https://docs.microsoft.com/en-us/windows/win32/devnotes/ntquerysymboliclinkobject dword_result_t NtQuerySymbolicLinkObject(dword_t handle, pointer_t target) { auto symlink = kernel_state()->object_table()->LookupObject(handle); if (!symlink) { return X_STATUS_NO_SUCH_FILE; } auto length = std::min(static_cast(target->maximum_length), symlink->target().size()); if (length > 0) { auto target_buf = kernel_memory()->TranslateVirtual(target->pointer); std::memcpy(target_buf, symlink->target().c_str(), length); } target->length = static_cast(length); return X_STATUS_SUCCESS; } DECLARE_XBOXKRNL_EXPORT1(NtQuerySymbolicLinkObject, kFileSystem, kImplemented); dword_result_t FscGetCacheElementCount(dword_t r3) { return 0; } DECLARE_XBOXKRNL_EXPORT1(FscGetCacheElementCount, kFileSystem, kStub); dword_result_t FscSetCacheElementCount(dword_t unk_0, dword_t unk_1) { // unk_0 = 0 // unk_1 looks like a count? in what units? 256 is a common value return X_STATUS_SUCCESS; } DECLARE_XBOXKRNL_EXPORT1(FscSetCacheElementCount, kFileSystem, kStub); void RegisterIoExports(xe::cpu::ExportResolver* export_resolver, KernelState* kernel_state) {} } // namespace xboxkrnl } // namespace kernel } // namespace xe