/** ****************************************************************************** * 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/vfs/virtual_file_system.h" #include "xenia/kernel/xam/content_manager.h" #include "xenia/vfs/devices/xcontent_container_device.h" #include "devices/host_path_entry.h" #include "xenia/base/literals.h" #include "xenia/base/logging.h" #include "xenia/base/string.h" #include "xenia/kernel/xfile.h" namespace xe { namespace vfs { using namespace xe::literals; VirtualFileSystem::VirtualFileSystem() {} VirtualFileSystem::~VirtualFileSystem() { // Delete all devices. // This will explode if anyone is still using data from them. Clear(); } void VirtualFileSystem::Clear() { devices_.clear(); symlinks_.clear(); } bool VirtualFileSystem::RegisterDevice(std::unique_ptr device) { auto global_lock = global_critical_region_.Acquire(); devices_.emplace_back(std::move(device)); return true; } bool VirtualFileSystem::UnregisterDevice(const std::string_view path) { auto global_lock = global_critical_region_.Acquire(); for (auto it = devices_.begin(); it != devices_.end(); ++it) { if ((*it)->mount_path() == path) { XELOGD("Unregistered device: {}", (*it)->mount_path()); devices_.erase(it); return true; } } return false; } bool VirtualFileSystem::RegisterSymbolicLink(const std::string_view path, const std::string_view target) { auto global_lock = global_critical_region_.Acquire(); auto it = std::find_if( symlinks_.cbegin(), symlinks_.cend(), [&](const auto& s) { return xe::utf8::equal_case(path, s.first); }); if (it != symlinks_.end()) { XELOGE("Trying to re-register already registered symbolic link: {} => {}", path, target); return false; } symlinks_.insert({std::string(path), std::string(target)}); XELOGD("Registered symbolic link: {} => {}", path, target); return true; } bool VirtualFileSystem::UnregisterSymbolicLink(const std::string_view path) { auto global_lock = global_critical_region_.Acquire(); auto it = std::find_if( symlinks_.cbegin(), symlinks_.cend(), [&](const auto& s) { return xe::utf8::equal_case(path, s.first); }); if (it == symlinks_.end()) { return false; } XELOGD("Unregistered symbolic link: {} => {}", it->first, it->second); symlinks_.erase(it); return true; } bool VirtualFileSystem::IsSymbolicLinkRegistered(const std::string_view path) { auto it = std::find_if( symlinks_.cbegin(), symlinks_.cend(), [&](const auto& s) { return xe::utf8::equal_case(path, s.first); }); return it != symlinks_.cend(); } bool VirtualFileSystem::FindSymbolicLink(const std::string_view path, std::string& target) { auto it = std::find_if( symlinks_.cbegin(), symlinks_.cend(), [&](const auto& s) { return xe::utf8::starts_with_case(path, s.first); }); if (it == symlinks_.cend()) { return false; } target = (*it).second; return true; } bool VirtualFileSystem::ResolveSymbolicLink(const std::string_view path, std::string& result) { result = path; bool was_resolved = false; while (true) { auto it = std::find_if(symlinks_.cbegin(), symlinks_.cend(), [&](const auto& s) { return xe::utf8::starts_with_case(result, s.first); }); if (it == symlinks_.cend()) { break; } // Found symlink! auto target_path = (*it).second; auto relative_path = result.substr((*it).first.size()); result = target_path + relative_path; was_resolved = true; } return was_resolved; } Entry* VirtualFileSystem::ResolvePath(const std::string_view path) { auto global_lock = global_critical_region_.Acquire(); // Resolve relative paths auto normalized_path(xe::utf8::canonicalize_guest_path(path)); // Resolve symlinks. std::string resolved_path; if (ResolveSymbolicLink(normalized_path, resolved_path)) { normalized_path = resolved_path; } // Find the device. auto it = std::find_if(devices_.cbegin(), devices_.cend(), [&](const auto& d) { return xe::utf8::starts_with(normalized_path, d->mount_path()); }); if (it == devices_.cend()) { // Supress logging the error for ShaderDumpxe:\CompareBackEnds as this is // not an actual problem nor something we care about. if (path != "ShaderDumpxe:\\CompareBackEnds") { XELOGE("ResolvePath({}) failed - device not found", path); } return nullptr; } const auto& device = *it; auto relative_path = normalized_path.substr(device->mount_path().size()); return device->ResolvePath(relative_path); } Entry* VirtualFileSystem::CreatePath(const std::string_view path, uint32_t attributes) { // Create all required directories recursively. auto path_parts = xe::utf8::split_path(path); if (path_parts.empty()) { return nullptr; } auto partial_path = std::string(path_parts[0]); auto partial_entry = ResolvePath(partial_path); if (!partial_entry) { return nullptr; } auto parent_entry = partial_entry; for (size_t i = 1; i < path_parts.size() - 1; ++i) { partial_path = xe::utf8::join_guest_paths(partial_path, path_parts[i]); auto child_entry = ResolvePath(partial_path); if (!child_entry) { child_entry = parent_entry->CreateEntry(path_parts[i], kFileAttributeDirectory); } if (!child_entry) { return nullptr; } parent_entry = child_entry; } return parent_entry->CreateEntry(path_parts[path_parts.size() - 1], attributes); } bool VirtualFileSystem::DeletePath(const std::string_view path) { auto entry = ResolvePath(path); if (!entry) { return false; } auto parent = entry->parent(); if (!parent) { // Can't delete root. return false; } return parent->Delete(entry); } X_STATUS VirtualFileSystem::OpenFile(Entry* root_entry, const std::string_view path, FileDisposition creation_disposition, uint32_t desired_access, bool is_directory, bool is_non_directory, File** out_file, FileAction* out_action) { // TODO(gibbed): should 'is_directory' remain as a bool or should it be // flipped to a generic FileAttributeFlags? // Cleanup access. if (desired_access & FileAccess::kGenericRead) { desired_access |= FileAccess::kFileReadData; } if (desired_access & FileAccess::kGenericWrite) { desired_access |= FileAccess::kFileWriteData; } if (desired_access & FileAccess::kGenericAll) { desired_access |= FileAccess::kFileReadData | FileAccess::kFileWriteData; } // Lookup host device/parent path. // If no device or parent, fail. Entry* parent_entry = nullptr; Entry* entry = nullptr; auto base_path = xe::utf8::find_base_guest_path(path); if (!base_path.empty()) { parent_entry = !root_entry ? ResolvePath(base_path) : root_entry->ResolvePath(base_path); if (!parent_entry) { *out_action = FileAction::kDoesNotExist; return X_STATUS_OBJECT_PATH_NOT_FOUND; } auto file_name = xe::utf8::find_name_from_guest_path(path); entry = parent_entry->GetChild(file_name); } else { entry = !root_entry ? ResolvePath(path) : root_entry->GetChild(path); } if (entry) { if (entry->attributes() & kFileAttributeDirectory && is_non_directory) { return X_STATUS_FILE_IS_A_DIRECTORY; } // If the entry does not exist on the host then remove the cached entry if (parent_entry) { const xe::vfs::HostPathEntry* host_path = dynamic_cast(parent_entry); if (host_path) { auto const file_path = host_path->host_path() / entry->name(); if (!std::filesystem::exists(file_path)) { // Remove cached entry entry->Delete(); entry = nullptr; } } } } // Check if exists (if we need it to), or that it doesn't (if it shouldn't). switch (creation_disposition) { case FileDisposition::kOpen: case FileDisposition::kOverwrite: // Must exist. if (!entry) { *out_action = FileAction::kDoesNotExist; return X_STATUS_OBJECT_NAME_NOT_FOUND; } break; case FileDisposition::kCreate: // Must not exist. if (entry) { *out_action = FileAction::kExists; return X_STATUS_OBJECT_NAME_COLLISION; } break; default: // Either way, ok. break; } // Verify permissions. bool wants_write = desired_access & FileAccess::kFileWriteData || desired_access & FileAccess::kFileAppendData; if (wants_write && ((parent_entry && parent_entry->is_read_only()) || (entry && entry->is_read_only()))) { // Fail if read only device and wants write. // return X_STATUS_ACCESS_DENIED; // TODO(benvanik): figure out why games are opening read-only files with // write modes. assert_always(); XELOGW("Attempted to open the file/dir for create/write"); desired_access = FileAccess::kGenericRead | FileAccess::kFileReadData; } bool created = false; if (!entry) { // Remember that we are creating this new, instead of replacing. created = true; *out_action = FileAction::kCreated; } else { // May need to delete, if it exists. switch (creation_disposition) { case FileDisposition::kCreate: // Shouldn't be possible to hit this. assert_always(); return X_STATUS_ACCESS_DENIED; case FileDisposition::kSuperscede: // Replace (by delete + recreate). if (!entry->Delete()) { return X_STATUS_ACCESS_DENIED; } entry = nullptr; *out_action = FileAction::kSuperseded; break; case FileDisposition::kOpen: case FileDisposition::kOpenIf: // Normal open. *out_action = FileAction::kOpened; break; case FileDisposition::kOverwrite: case FileDisposition::kOverwriteIf: // Overwrite (we do by delete + recreate). if (!entry->Delete()) { return X_STATUS_ACCESS_DENIED; } entry = nullptr; *out_action = FileAction::kOverwritten; break; } } if (!entry) { // Create if needed (either new or as a replacement). entry = CreatePath( path, !is_directory ? kFileAttributeNormal : kFileAttributeDirectory); if (!entry) { return X_STATUS_ACCESS_DENIED; } } // Open. auto result = entry->Open(desired_access, out_file); if (XFAILED(result)) { *out_action = FileAction::kDoesNotExist; } return result; } X_STATUS VirtualFileSystem::ExtractContentFile(Entry* entry, std::filesystem::path base_path, uint64_t& progress, bool extract_to_root) { // Allocate a buffer when needed. size_t buffer_size = 0; uint8_t* buffer = nullptr; XELOGI("Extracting file: {}", entry->path()); auto dest_name = base_path / xe::to_path(utf8::fix_path_separators(entry->path())); if (extract_to_root) { dest_name = base_path / xe::to_path(entry->name()); } if (entry->attributes() & kFileAttributeDirectory) { std::error_code error_code; std::filesystem::create_directories(dest_name, error_code); if (error_code) { return error_code.value(); } return 0; } vfs::File* in_file = nullptr; X_STATUS result = entry->Open(FileAccess::kFileReadData, &in_file); if (result != X_STATUS_SUCCESS) { return result; } auto file = xe::filesystem::OpenFile(dest_name, "wb"); if (!file) { in_file->Destroy(); return 1; } constexpr size_t write_buffer_size = 4_MiB; if (entry->can_map()) { auto map = entry->OpenMapped(xe::MappedMemory::Mode::kRead); size_t remaining_size = map->size(); size_t offset = 0; while (remaining_size > 0) { const auto bytes_to_read = std::min(write_buffer_size, remaining_size); fwrite(map->data() + offset, bytes_to_read, 1, file); offset += bytes_to_read; remaining_size -= bytes_to_read; progress += bytes_to_read; } map->Close(); } else { size_t remaining_size = entry->size(); size_t offset = 0; buffer = new uint8_t[write_buffer_size]; while (remaining_size > 0) { size_t bytes_read = 0; in_file->ReadSync(std::span(buffer, write_buffer_size), offset, &bytes_read); fwrite(buffer, bytes_read, 1, file); offset += bytes_read; remaining_size -= bytes_read; progress += bytes_read; } } fclose(file); in_file->Destroy(); if (buffer) { delete[] buffer; } return 0; } X_STATUS VirtualFileSystem::ExtractContentFiles(Device* device, std::filesystem::path base_path, uint64_t& progress) { // Run through all the files, breadth-first style. std::queue queue; auto root = device->ResolvePath("/"); queue.push(root); while (!queue.empty()) { auto entry = queue.front(); queue.pop(); for (auto& entry : entry->children()) { queue.push(entry.get()); } ExtractContentFile(entry, base_path, progress); } return X_STATUS_SUCCESS; } void VirtualFileSystem::ExtractContentHeader(Device* device, std::filesystem::path base_path) { const XContentContainerDevice* xcontent_device = ((XContentContainerDevice*)device); if (!std::filesystem::exists(base_path.parent_path())) { if (!std::filesystem::create_directories(base_path.parent_path())) { return; } } auto header_filename = base_path.filename().string() + ".header"; auto header_path = base_path.parent_path() / header_filename; xe::filesystem::CreateEmptyFile(header_path); if (std::filesystem::exists(header_path)) { auto file = xe::filesystem::OpenFile(header_path, "wb"); kernel::xam::XCONTENT_AGGREGATE_DATA data = xcontent_device->content_header(); uint32_t license_mask = xcontent_device->license_mask(); data.set_file_name(base_path.filename().string()); fwrite(&data, 1, sizeof(kernel::xam::XCONTENT_AGGREGATE_DATA), file); fwrite(&license_mask, 1, sizeof(license_mask), file); fclose(file); } return; } } // namespace vfs } // namespace xe