diff --git a/src/xenia/kernel/info/file.h b/src/xenia/kernel/info/file.h index 9305927bc..26557bb51 100644 --- a/src/xenia/kernel/info/file.h +++ b/src/xenia/kernel/info/file.h @@ -64,6 +64,14 @@ struct X_FILE_INTERNAL_INFORMATION { }; static_assert_size(X_FILE_INTERNAL_INFORMATION, 8); +// https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/ns-ntifs-_file_rename_information +struct X_FILE_RENAME_INFORMATION { + be replace_existing; + be root_dir_handle; + X_ANSI_STRING ansi_string; +}; +static_assert_size(X_FILE_RENAME_INFORMATION, 16); + // https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/ntddk/ns-ntddk-_file_disposition_information struct X_FILE_DISPOSITION_INFORMATION { uint8_t delete_file; diff --git a/src/xenia/kernel/xboxkrnl/xboxkrnl_io.cc b/src/xenia/kernel/xboxkrnl/xboxkrnl_io.cc index 636eb4a3e..f6b89345d 100644 --- a/src/xenia/kernel/xboxkrnl/xboxkrnl_io.cc +++ b/src/xenia/kernel/xboxkrnl/xboxkrnl_io.cc @@ -39,62 +39,6 @@ struct CreateOptions { 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 . (*.) - // - // 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_entry(lpdword_t handle_out, dword_t desired_access, pointer_t object_attrs, pointer_t io_status_block, diff --git a/src/xenia/kernel/xboxkrnl/xboxkrnl_io_info.cc b/src/xenia/kernel/xboxkrnl/xboxkrnl_io_info.cc index d042a723a..e6aae5d43 100644 --- a/src/xenia/kernel/xboxkrnl/xboxkrnl_io_info.cc +++ b/src/xenia/kernel/xboxkrnl/xboxkrnl_io_info.cc @@ -91,6 +91,13 @@ dword_result_t NtQueryInformationFile_entry( out_length = sizeof(*info); break; } + case XFileAlignmentInformation: { + // Requested by XMountUtilityDrive XAM-task + auto info = info_ptr.as(); + *info = 0; // FILE_BYTE_ALIGNMENT? + out_length = sizeof(*info); + break; + } case XFileSectorInformation: { // SW that uses this seems to use the output as a way of uniquely // identifying a file for sorting/lookup so we can just give it an @@ -130,13 +137,6 @@ dword_result_t NtQueryInformationFile_entry( out_length = sizeof(*info); break; } - case XFileAlignmentInformation: { - // Requested by XMountUtilityDrive XAM-task - auto info = info_ptr.as(); - *info = 0; // FILE_BYTE_ALIGNMENT? - out_length = sizeof(*info); - break; - } default: { // Unsupported, for now. assert_always(); @@ -157,6 +157,8 @@ DECLARE_XBOXKRNL_EXPORT1(NtQueryInformationFile, kFileSystem, kImplemented); uint32_t GetSetFileInfoMinimumLength(uint32_t info_class) { switch (info_class) { + case XFileRenameInformation: + return sizeof(X_FILE_RENAME_INFORMATION); case XFileDispositionInformation: return sizeof(X_FILE_DISPOSITION_INFORMATION); case XFilePositionInformation: @@ -171,7 +173,6 @@ uint32_t GetSetFileInfoMinimumLength(uint32_t info_class) { case XFileEndOfFileInformation: case XFileMountPartitionInformation: return 8; - case XFileRenameInformation: case XFileLinkInformation: return 16; case XFileBasicInformation: @@ -204,6 +205,25 @@ dword_result_t NtSetInformationFile_entry( uint32_t out_length; switch (info_class) { + case XFileRenameInformation: { + auto info = info_ptr.as(); + // Compute path, possibly attrs relative. + std::filesystem::path target_path = + util::TranslateAnsiString(kernel_memory(), &info->ansi_string); + + // Place IsValidPath in path from where it can be accessed everywhere + if (!IsValidPath(target_path.string(), false)) { + return X_STATUS_OBJECT_NAME_INVALID; + } + + if (!target_path.has_filename()) { + return X_STATUS_INVALID_PARAMETER; + } + + file->Rename(target_path); + out_length = sizeof(*info); + break; + } case XFileDispositionInformation: { // Used to set deletion flag. Which we don't support. Probably? auto info = info_ptr.as(); diff --git a/src/xenia/kernel/xfile.cc b/src/xenia/kernel/xfile.cc index f46a9e137..a1c9257d2 100644 --- a/src/xenia/kernel/xfile.cc +++ b/src/xenia/kernel/xfile.cc @@ -268,6 +268,10 @@ X_STATUS XFile::Write(uint32_t buffer_guest_address, uint32_t buffer_length, } X_STATUS XFile::SetLength(size_t length) { return file_->SetLength(length); } +X_STATUS XFile::Rename(const std::filesystem::path file_path) { + entry()->Rename(file_path); + return X_STATUS_SUCCESS; +} void XFile::RegisterIOCompletionPort(uint32_t key, object_ref port) { diff --git a/src/xenia/kernel/xfile.h b/src/xenia/kernel/xfile.h index a5130da8d..1fea81a81 100644 --- a/src/xenia/kernel/xfile.h +++ b/src/xenia/kernel/xfile.h @@ -73,6 +73,62 @@ class X_FILE_DIRECTORY_INFORMATION { } }; +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 . (*.) + // + // 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; +} + class XFile : public XObject { public: static const XObject::Type kObjectType = XObject::Type::File; @@ -110,6 +166,7 @@ class XFile : public XObject { uint32_t apc_context); X_STATUS SetLength(size_t length); + X_STATUS Rename(const std::filesystem::path file_path); void RegisterIOCompletionPort(uint32_t key, object_ref port); void RemoveIOCompletionPort(uint32_t key); diff --git a/src/xenia/vfs/devices/host_path_device.h b/src/xenia/vfs/devices/host_path_device.h index cf5d101c4..0adbc97d5 100644 --- a/src/xenia/vfs/devices/host_path_device.h +++ b/src/xenia/vfs/devices/host_path_device.h @@ -40,6 +40,10 @@ class HostPathDevice : public Device { uint32_t sectors_per_allocation_unit() const override { return 1; } uint32_t bytes_per_sector() const override { return 0x200; } + protected: + friend class HostPathEntry; + std::filesystem::path host_path() const { return host_path_; } + private: void PopulateEntry(HostPathEntry* parent_entry); diff --git a/src/xenia/vfs/devices/host_path_entry.cc b/src/xenia/vfs/devices/host_path_entry.cc index b260e6b64..394b445b9 100644 --- a/src/xenia/vfs/devices/host_path_entry.cc +++ b/src/xenia/vfs/devices/host_path_entry.cc @@ -15,6 +15,7 @@ #include "xenia/base/math.h" #include "xenia/base/string.h" #include "xenia/vfs/device.h" +#include "xenia/vfs/devices/host_path_device.h" #include "xenia/vfs/devices/host_path_file.h" namespace xe { @@ -110,6 +111,15 @@ bool HostPathEntry::DeleteEntryInternal(Entry* entry) { } } +void HostPathEntry::RenameEntryInternal(const std::filesystem::path file_path) { + const std::string new_host_path_ = xe::utf8::join_paths( + xe::path_to_utf8(((HostPathDevice*)device_)->host_path()), + xe::path_to_utf8(file_path)); + + std::filesystem::rename(host_path_, new_host_path_); + host_path_ = new_host_path_; +} + void HostPathEntry::update() { xe::filesystem::FileInfo file_info; if (!xe::filesystem::GetInfo(host_path_, &file_info)) { diff --git a/src/xenia/vfs/devices/host_path_entry.h b/src/xenia/vfs/devices/host_path_entry.h index 0a1b3e80d..c43cdde44 100644 --- a/src/xenia/vfs/devices/host_path_entry.h +++ b/src/xenia/vfs/devices/host_path_entry.h @@ -46,6 +46,7 @@ class HostPathEntry : public Entry { std::unique_ptr CreateEntryInternal(const std::string_view name, uint32_t attributes) override; bool DeleteEntryInternal(Entry* entry) override; + void RenameEntryInternal(const std::filesystem::path file_path) override; std::filesystem::path host_path_; }; diff --git a/src/xenia/vfs/entry.cc b/src/xenia/vfs/entry.cc index 7f9017608..56a6b7def 100644 --- a/src/xenia/vfs/entry.cc +++ b/src/xenia/vfs/entry.cc @@ -133,5 +133,22 @@ void Entry::Touch() { // TODO(benvanik): update timestamps. } +void Entry::Rename(const std::filesystem::path file_path) { + std::vector splitted_path = + xe::utf8::split_path(xe::path_to_utf8(file_path)); + + splitted_path.erase(splitted_path.begin()); + + const std::string guest_path_without_root = + xe::utf8::join_guest_paths(splitted_path); + + RenameEntryInternal(guest_path_without_root); + + absolute_path_ = xe::utf8::join_guest_paths(device_->mount_path(), + guest_path_without_root); + path_ = guest_path_without_root; + name_ = xe::path_to_utf8(file_path.filename()); +} + } // namespace vfs } // namespace xe diff --git a/src/xenia/vfs/entry.h b/src/xenia/vfs/entry.h index d4dee331d..554c696d2 100644 --- a/src/xenia/vfs/entry.h +++ b/src/xenia/vfs/entry.h @@ -111,6 +111,7 @@ class Entry { bool Delete(); void Touch(); + void Rename(const std::filesystem::path file_path); // If successful, out_file points to a new file. When finished, call // file->Destroy() virtual X_STATUS Open(uint32_t desired_access, File** out_file) = 0; @@ -131,6 +132,7 @@ class Entry { return nullptr; } virtual bool DeleteEntryInternal(Entry* entry) { return false; } + virtual void RenameEntryInternal(const std::filesystem::path file_path) {} xe::global_critical_region global_critical_region_; Device* device_; diff --git a/src/xenia/vfs/file.h b/src/xenia/vfs/file.h index af06c1b45..841a9ed6f 100644 --- a/src/xenia/vfs/file.h +++ b/src/xenia/vfs/file.h @@ -45,6 +45,9 @@ class File { } virtual X_STATUS SetLength(size_t length) { return X_STATUS_NOT_IMPLEMENTED; } + virtual X_STATUS Rename(const std::filesystem::path file_path) { + return X_STATUS_NOT_IMPLEMENTED; + } // xe::filesystem::FileAccess uint32_t file_access() const { return file_access_; }