From 783b180dc8d112a036130437ffaf9c8f2ad0f8c9 Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Thu, 23 Sep 2021 03:22:17 +0200 Subject: Core: Add RiivolutionPatcher to apply Riivolution-style patches to a game's file system and memory. --- Source/Core/DiscIO/RiivolutionPatcher.cpp | 428 ++++++++++++++++++++++++++++++ 1 file changed, 428 insertions(+) create mode 100644 Source/Core/DiscIO/RiivolutionPatcher.cpp (limited to 'Source/Core/DiscIO/RiivolutionPatcher.cpp') diff --git a/Source/Core/DiscIO/RiivolutionPatcher.cpp b/Source/Core/DiscIO/RiivolutionPatcher.cpp new file mode 100644 index 0000000000..b62d6d3ff4 --- /dev/null +++ b/Source/Core/DiscIO/RiivolutionPatcher.cpp @@ -0,0 +1,428 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "DiscIO/RiivolutionPatcher.h" + +#include +#include +#include +#include +#include +#include + +#include "Common/FileUtil.h" +#include "Common/IOFile.h" +#include "Common/StringUtil.h" +#include "Core/HW/Memmap.h" +#include "Core/PowerPC/MMU.h" +#include "DiscIO/DirectoryBlob.h" +#include "DiscIO/RiivolutionParser.h" + +namespace DiscIO::Riivolution +{ +// 'before' and 'after' should be two copies of the same source +// 'split_at' needs to be between the start and end of the source, may not match either boundary +static void SplitAt(BuilderContentSource* before, BuilderContentSource* after, u64 split_at) +{ + const u64 start = before->m_offset; + const u64 size = before->m_size; + const u64 end = start + size; + + // The source before the split point just needs its length reduced. + before->m_size = split_at - start; + + // The source after the split needs its length reduced and its start point adjusted. + after->m_offset += before->m_size; + after->m_size = end - split_at; + if (std::holds_alternative(after->m_source)) + std::get(after->m_source).m_offset += before->m_size; + else if (std::holds_alternative(after->m_source)) + std::get(after->m_source) += before->m_size; + else if (std::holds_alternative(after->m_source)) + std::get(after->m_source).m_offset += before->m_size; + else if (std::holds_alternative(after->m_source)) + std::get(after->m_source).m_offset += before->m_size; +} + +static void ApplyPatchToFile(const Patch& patch, DiscIO::FSTBuilderNode* file_node, + std::string external_filename, u64 file_patch_offset, + u64 raw_external_file_offset, u64 file_patch_length, bool resize) +{ + ::File::IOFile f(external_filename, "rb"); + if (!f) + return; + + auto& content = std::get>(file_node->m_content); + + const u64 raw_external_filesize = f.GetSize(); + const u64 external_file_offset = std::min(raw_external_file_offset, raw_external_filesize); + const u64 external_filesize = raw_external_filesize - external_file_offset; + + const u64 patch_start = file_patch_offset; + const u64 patch_size = file_patch_length == 0 ? external_filesize : file_patch_length; + const u64 patch_end = patch_start + patch_size; + + const u64 target_filesize = resize ? patch_end : std::max(file_node->m_size, patch_end); + + size_t insert_where = 0; + if (patch_start >= file_node->m_size) + { + // If the patch is at or past the end of the existing file no existing content needs to be + // touched, just extend the file. + if (patch_start > file_node->m_size) + { + // Insert an padding area between the old file and the patch data. + content.emplace_back(BuilderContentSource{file_node->m_size, patch_start - file_node->m_size, + ContentFixedByte{0}}); + } + + insert_where = content.size(); + } + else + { + // Patch is at the start or somewhere in the middle of the existing file. At least one source + // needs to be modified or removed, and a new source with the patch data inserted instead. + // To make this easier, we first split up existing sources at the patch start and patch end + // offsets, then discard all overlapping sources and insert the patch sources there. + for (size_t i = 0; i < content.size(); ++i) + { + const u64 source_start = content[i].m_offset; + const u64 source_end = source_start + content[i].m_size; + if (patch_start > source_start && patch_start < source_end) + { + content.insert(content.begin() + i + 1, content[i]); + SplitAt(&content[i], &content[i + 1], patch_start); + continue; + } + if (patch_end > source_start && patch_end < source_end) + { + content.insert(content.begin() + i + 1, content[i]); + SplitAt(&content[i], &content[i + 1], patch_end); + } + } + + // Now discard the overlapping areas and remember where they were so we can insert there. + for (size_t i = 0; i < content.size(); ++i) + { + if (patch_start == content[i].m_offset) + { + insert_where = i; + while (i < content.size() && patch_end >= content[i].m_offset + content[i].m_size) + ++i; + content.erase(content.begin() + insert_where, content.begin() + i); + break; + } + } + } + + // Insert the actual patch data. + if (patch_size > 0 && external_filesize > 0) + { + BuilderContentSource source{patch_start, std::min(patch_size, external_filesize), + ContentFile{std::move(external_filename), external_file_offset}}; + content.emplace(content.begin() + insert_where, std::move(source)); + ++insert_where; + } + + // Pad with zeroes if the patch file is smaller than the patch size. + if (external_filesize < patch_size) + { + BuilderContentSource padding{patch_start + external_filesize, patch_size - external_filesize, + ContentFixedByte{0}}; + content.emplace(content.begin() + insert_where, std::move(padding)); + } + + // Update the filesize of the file. + file_node->m_size = target_filesize; + + // Drop any source past the new end of the file -- this can happen on file truncation. + while (!content.empty() && content.back().m_offset >= target_filesize) + content.pop_back(); +} + +static void ApplyPatchToFile(const Patch& patch, const File& file_patch, + DiscIO::FSTBuilderNode* file_node) +{ + ApplyPatchToFile(patch, file_node, patch.m_root + "/" + file_patch.m_external, + file_patch.m_offset, file_patch.m_fileoffset, file_patch.m_length, + file_patch.m_resize); +} + +static bool CaseInsensitiveEquals(std::string_view a, std::string_view b) +{ + if (a.size() != b.size()) + return false; + return std::equal(a.begin(), a.end(), b.begin(), [](char ca, char cb) { + return std::tolower(ca, std::locale::classic()) == std::tolower(cb, std::locale::classic()); + }); +} + +static FSTBuilderNode* FindFileNodeInFST(std::string_view path, std::vector* fst, + bool create_if_not_exists) +{ + const size_t path_separator = path.find('/'); + const bool is_file = path_separator == std::string_view::npos; + const std::string_view name = is_file ? path : path.substr(0, path_separator); + const auto it = std::find_if(fst->begin(), fst->end(), [&](const FSTBuilderNode& node) { + return CaseInsensitiveEquals(node.m_filename, name); + }); + + if (it == fst->end()) + { + if (!create_if_not_exists) + return nullptr; + + if (is_file) + { + return &fst->emplace_back( + DiscIO::FSTBuilderNode{std::string(name), 0, std::vector()}); + } + + auto& new_folder = fst->emplace_back( + DiscIO::FSTBuilderNode{std::string(name), 0, std::vector()}); + return FindFileNodeInFST(path.substr(path_separator + 1), + &std::get>(new_folder.m_content), true); + } + + const bool is_existing_node_file = it->IsFile(); + if (is_file != is_existing_node_file) + return nullptr; + if (is_file) + return &*it; + + return FindFileNodeInFST(path.substr(path_separator + 1), + &std::get>(it->m_content), + create_if_not_exists); +} + +static void FindFilenameNodesInFST(std::vector* nodes_out, + std::string_view filename, std::vector* fst) +{ + for (FSTBuilderNode& node : *fst) + { + if (node.IsFolder()) + { + FindFilenameNodesInFST(nodes_out, filename, + &std::get>(node.m_content)); + } + else if (node.m_filename == filename) + { + nodes_out->push_back(&node); + } + } +} + +static void ApplyFolderPatchToFST(const Patch& patch, const Folder& folder, + const ::File::FSTEntry& external_files, + std::string_view disc_path, + std::vector* fst) +{ + for (const auto& child : external_files.children) + { + std::string child_disc_patch = std::string(disc_path) + "/" + child.virtualName; + if (child.isDirectory) + { + ApplyFolderPatchToFST(patch, folder, child, child_disc_patch, fst); + } + else + { + DiscIO::FSTBuilderNode* node = FindFileNodeInFST(child_disc_patch, fst, folder.m_create); + if (node) + ApplyPatchToFile(patch, node, child.physicalName, 0, 0, folder.m_length, folder.m_resize); + } + } +} + +static void ApplyUnknownFolderPatchToFST(const Patch& patch, const Folder& folder, + const ::File::FSTEntry& external_files, + std::vector* fst) +{ + for (const auto& child : external_files.children) + { + if (child.isDirectory) + { + ApplyUnknownFolderPatchToFST(patch, folder, child, fst); + } + else + { + std::vector nodes; + FindFilenameNodesInFST(&nodes, child.virtualName, fst); + for (auto* node : nodes) + ApplyPatchToFile(patch, node, child.physicalName, 0, 0, folder.m_length, folder.m_resize); + } + } +} + +void ApplyPatchesToFiles(const std::vector& patches, + std::vector* fst, DiscIO::FSTBuilderNode* dol_node) +{ + // For file searching purposes, Riivolution assumes that the game's main.dol is in the root of the + // file system. So to avoid doing a bunch of special case handling for that, we just put a node + // for this into the FST and remove it again after the file patching is done. + dol_node->m_filename = "main.dol"; + fst->push_back(*dol_node); + + for (const auto& patch : patches) + { + for (const auto& file : patch.m_file_patches) + { + if (!file.m_disc.empty() && file.m_disc[0] == '/') + { + // If the disc path starts with a / then we should patch that specific disc path. + DiscIO::FSTBuilderNode* node = + FindFileNodeInFST(std::string_view(file.m_disc).substr(1), fst, file.m_create); + if (node) + ApplyPatchToFile(patch, file, node); + } + else + { + // Otherwise we want to patch any file on the entire disc matching that filename. + std::vector nodes; + FindFilenameNodesInFST(&nodes, file.m_disc, fst); + for (auto* node : nodes) + ApplyPatchToFile(patch, file, node); + } + } + + for (const auto& folder : patch.m_folder_patches) + { + ::File::FSTEntry external_files = + ::File::ScanDirectoryTree(patch.m_root + "/" + folder.m_external, folder.m_recursive); + + std::string_view disc_path = folder.m_disc; + while (StringBeginsWith(disc_path, "/")) + disc_path.remove_prefix(1); + while (StringEndsWith(disc_path, "/")) + disc_path.remove_suffix(1); + if (disc_path.empty()) + ApplyUnknownFolderPatchToFST(patch, folder, external_files, fst); + else + ApplyFolderPatchToFST(patch, folder, external_files, disc_path, fst); + } + } + + auto main_dol_node_in_fst = + std::find_if(fst->begin(), fst->end(), [&](const DiscIO::FSTBuilderNode& node) { + return node.m_filename == "main.dol"; + }); + if (main_dol_node_in_fst != fst->end()) + { + *dol_node = *main_dol_node_in_fst; + fst->erase(main_dol_node_in_fst); + } +} + +static bool MemoryMatchesAt(u32 offset, const std::vector& value) +{ + for (u32 i = 0; i < value.size(); ++i) + { + auto result = PowerPC::HostTryReadU8(offset + i); + if (!result || result->value != value[i]) + return false; + } + return true; +} + +static void ApplyMemoryPatch(u32 offset, const std::vector& value, + const std::vector& original) +{ + if (value.empty()) + return; + + if (!original.empty() && !MemoryMatchesAt(offset, original)) + return; + + for (u32 i = 0; i < value.size(); ++i) + PowerPC::HostTryWriteU8(value[i], offset + i); +} + +static std::vector GetMemoryPatchValue(const Patch& patch, const Memory& memory_patch) +{ + if (!memory_patch.m_valuefile.empty()) + { + ::File::IOFile f(patch.m_root + "/" + memory_patch.m_valuefile, "rb"); + if (!f) + return {}; + const u64 length = f.GetSize(); + std::vector value; + value.resize(length); + if (!f.ReadBytes(value.data(), length)) + return {}; + + return value; + } + + return memory_patch.m_value; +} + +static void ApplyMemoryPatch(const Patch& patch, const Memory& memory_patch) +{ + ApplyMemoryPatch(memory_patch.m_offset | 0x80000000, GetMemoryPatchValue(patch, memory_patch), + memory_patch.m_original); +} + +static void ApplySearchMemoryPatch(const Patch& patch, const Memory& memory_patch) +{ + if (memory_patch.m_original.empty()) + return; + + const u32 ram_size = ::Memory::GetRamSize(); + const u32 stride = memory_patch.m_align < 1 ? 1 : memory_patch.m_align; + for (u32 i = 0; i < ram_size - (stride - 1); i += stride) + { + const u32 address = i | 0x80000000; + if (MemoryMatchesAt(address, memory_patch.m_original)) + { + ApplyMemoryPatch(address, GetMemoryPatchValue(patch, memory_patch), {}); + break; + } + } +} + +static void ApplyOcarinaMemoryPatch(const Patch& patch, const Memory& memory_patch) +{ + if (memory_patch.m_value.empty()) + return; + + const u32 ram_size = ::Memory::GetRamSize(); + for (u32 i = 0; i < ram_size; i += 4) + { + // first find the pattern + const u32 address = i | 0x80000000; + if (MemoryMatchesAt(address, memory_patch.m_value)) + { + for (; i < ram_size; i += 4) + { + // from the pattern find the next blr instruction + const u32 blr_address = i | 0x80000000; + auto blr = PowerPC::HostTryReadU32(blr_address); + if (blr && blr->value == 0x4e800020) + { + // and replace it with a jump to the given offset + const u32 target = memory_patch.m_offset | 0x80000000; + const u32 jmp = ((target - blr_address) & 0x03fffffc) | 0x48000000; + PowerPC::HostTryWriteU32(jmp, blr_address); + return; + } + } + return; + } + } +} + +void ApplyPatchesToMemory(const std::vector& patches) +{ + for (const auto& patch : patches) + { + for (const auto& memory : patch.m_memory_patches) + { + if (memory.m_ocarina) + ApplyOcarinaMemoryPatch(patch, memory); + else if (memory.m_search) + ApplySearchMemoryPatch(patch, memory); + else + ApplyMemoryPatch(patch, memory); + } + } +} +} // namespace DiscIO::Riivolution -- cgit v1.2.3 From 6ec4af7ea413ace595290d336bd88b30e035fa2f Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Tue, 28 Sep 2021 06:12:45 +0200 Subject: RiivolutionPatcher: Load external files with a layer of indirection during the patching process to properly resolve the paths given in the XML. This also may eventually allow loading patches from sources other than the 1:1 expected file structure host file system, such as memory or an archive file. --- Source/Core/DiscIO/RiivolutionPatcher.cpp | 238 +++++++++++++++++++++++++----- 1 file changed, 200 insertions(+), 38 deletions(-) (limited to 'Source/Core/DiscIO/RiivolutionPatcher.cpp') diff --git a/Source/Core/DiscIO/RiivolutionPatcher.cpp b/Source/Core/DiscIO/RiivolutionPatcher.cpp index b62d6d3ff4..0af7ad93b7 100644 --- a/Source/Core/DiscIO/RiivolutionPatcher.cpp +++ b/Source/Core/DiscIO/RiivolutionPatcher.cpp @@ -20,6 +20,160 @@ namespace DiscIO::Riivolution { +FileDataLoader::~FileDataLoader() = default; + +FileDataLoaderHostFS::FileDataLoaderHostFS(std::string sd_root, const std::string& xml_path, + std::string_view patch_root) + : m_sd_root(std::move(sd_root)) +{ + // Riivolution treats 'external' file paths as follows: + // - If it starts with a '/', it's an absolute path, ie. relative to the SD card root. + // - Otherwise: + // - If the 'root' parameter of the current patch is not set or is empty, the path is relative + // to the folder the XML file is in. + // - If the 'root' parameter of the current patch starts with a '/', the path is relative to + // that folder on the SD card, starting at the SD card root. + // - If the 'root' parameter of the current patch starts without a '/', the path is relative to + // that folder on the SD card, starting at the folder the XML file is in. + // The following initialization should properly replicate this behavior. + + // First set m_patch_root to the folder the parsed XML file is in. + SplitPath(xml_path, &m_patch_root, nullptr, nullptr); + + // Then try to resolve the given patch_root as if it was a file path, and on success replace the + // m_patch_root with it. + if (!patch_root.empty()) + { + auto r = MakeAbsoluteFromRelative(patch_root); + if (r) + m_patch_root = std::move(*r); + } +} + +std::optional +FileDataLoaderHostFS::MakeAbsoluteFromRelative(std::string_view external_relative_path) +{ +#ifdef _WIN32 + // Riivolution treats a backslash as just a standard filename character, but we can't replicate + // this properly on Windows. So if a file contains a backslash, immediately error out. + if (external_relative_path.find("\\") != std::string_view::npos) + return std::nullopt; +#endif + + std::string result = StringBeginsWith(external_relative_path, "/") ? m_sd_root : m_patch_root; + std::string_view work = external_relative_path; + + // Strip away all leading and trailing path separators. + while (StringBeginsWith(work, "/")) + work.remove_prefix(1); + while (StringEndsWith(work, "/")) + work.remove_suffix(1); + size_t depth = 0; + while (true) + { + if (work.empty()) + break; + + // Extract a single path element. + size_t separator_position = work.find('/'); + std::string_view element = work.substr(0, separator_position); + + if (element == ".") + { + // This is a harmless element, doesn't change any state. + } + else if (element == "..") + { + // We're going up a level. + // If this isn't possible someone is trying to exit the root directory, prevent that. + if (depth == 0) + return std::nullopt; + --depth; + + // Remove the last path element from the result string. + // This must have been previously attached in the branch below (otherwise depth would have + // been 0), so there's no need to check whether the string is empty or anything like that. + while (result.back() != '/') + result.pop_back(); + result.pop_back(); + } + else + { + // We're going down a level. + ++depth; + + // Append path element to result string. + result += '/'; + result += element; + } + + // If this was the last path element, we're done. + if (separator_position == std::string_view::npos) + break; + + // Remove element from work string. + work = work.substr(separator_position + 1); + + // Remove any potential extra path separators. + while (StringBeginsWith(work, "/")) + work = work.substr(1); + } + return result; +} + +std::optional +FileDataLoaderHostFS::GetExternalFileSize(std::string_view external_relative_path) +{ + auto path = MakeAbsoluteFromRelative(external_relative_path); + if (!path) + return std::nullopt; + ::File::IOFile f(*path, "rb"); + if (!f) + return std::nullopt; + return f.GetSize(); +} + +std::vector FileDataLoaderHostFS::GetFileContents(std::string_view external_relative_path) +{ + auto path = MakeAbsoluteFromRelative(external_relative_path); + if (!path) + return {}; + ::File::IOFile f(*path, "rb"); + if (!f) + return {}; + const u64 length = f.GetSize(); + std::vector value; + value.resize(length); + if (!f.ReadBytes(value.data(), length)) + return {}; + return value; +} + +std::vector +FileDataLoaderHostFS::GetFolderContents(std::string_view external_relative_path) +{ + auto path = MakeAbsoluteFromRelative(external_relative_path); + if (!path) + return {}; + ::File::FSTEntry external_files = ::File::ScanDirectoryTree(*path, false); + std::vector nodes; + nodes.reserve(external_files.children.size()); + for (auto& file : external_files.children) + nodes.emplace_back(FileDataLoader::Node{std::move(file.virtualName), file.isDirectory}); + return nodes; +} + +BuilderContentSource +FileDataLoaderHostFS::MakeContentSource(std::string_view external_relative_path, + u64 external_offset, u64 external_size, u64 disc_offset) +{ + auto path = MakeAbsoluteFromRelative(external_relative_path); + if (!path) + return BuilderContentSource{disc_offset, external_size, ContentFixedByte{0}}; + return BuilderContentSource{disc_offset, external_size, + ContentFile{std::move(*path), external_offset}}; +} + // 'before' and 'after' should be two copies of the same source // 'split_at' needs to be between the start and end of the source, may not match either boundary static void SplitAt(BuilderContentSource* before, BuilderContentSource* after, u64 split_at) @@ -45,16 +199,16 @@ static void SplitAt(BuilderContentSource* before, BuilderContentSource* after, u } static void ApplyPatchToFile(const Patch& patch, DiscIO::FSTBuilderNode* file_node, - std::string external_filename, u64 file_patch_offset, + std::string_view external_filename, u64 file_patch_offset, u64 raw_external_file_offset, u64 file_patch_length, bool resize) { - ::File::IOFile f(external_filename, "rb"); + const auto f = patch.m_file_data_loader->GetExternalFileSize(external_filename); if (!f) return; auto& content = std::get>(file_node->m_content); - const u64 raw_external_filesize = f.GetSize(); + const u64 raw_external_filesize = *f; const u64 external_file_offset = std::min(raw_external_file_offset, raw_external_filesize); const u64 external_filesize = raw_external_filesize - external_file_offset; @@ -118,8 +272,9 @@ static void ApplyPatchToFile(const Patch& patch, DiscIO::FSTBuilderNode* file_no // Insert the actual patch data. if (patch_size > 0 && external_filesize > 0) { - BuilderContentSource source{patch_start, std::min(patch_size, external_filesize), - ContentFile{std::move(external_filename), external_file_offset}}; + BuilderContentSource source = patch.m_file_data_loader->MakeContentSource( + external_filename, external_file_offset, std::min(patch_size, external_filesize), + patch_start); content.emplace(content.begin() + insert_where, std::move(source)); ++insert_where; } @@ -143,9 +298,8 @@ static void ApplyPatchToFile(const Patch& patch, DiscIO::FSTBuilderNode* file_no static void ApplyPatchToFile(const Patch& patch, const File& file_patch, DiscIO::FSTBuilderNode* file_node) { - ApplyPatchToFile(patch, file_node, patch.m_root + "/" + file_patch.m_external, - file_patch.m_offset, file_patch.m_fileoffset, file_patch.m_length, - file_patch.m_resize); + ApplyPatchToFile(patch, file_node, file_patch.m_external, file_patch.m_offset, + file_patch.m_fileoffset, file_patch.m_length, file_patch.m_resize); } static bool CaseInsensitiveEquals(std::string_view a, std::string_view b) @@ -213,42 +367,56 @@ static void FindFilenameNodesInFST(std::vector* nodes_o } static void ApplyFolderPatchToFST(const Patch& patch, const Folder& folder, - const ::File::FSTEntry& external_files, + const std::vector& external_files, + const std::string& external_path, bool recursive, std::string_view disc_path, std::vector* fst) { - for (const auto& child : external_files.children) + for (const auto& child : external_files) { - std::string child_disc_patch = std::string(disc_path) + "/" + child.virtualName; - if (child.isDirectory) + const std::string child_disc_path = std::string(disc_path) + "/" + child.m_filename; + const std::string child_external_path = external_path + "/" + child.m_filename; + if (child.m_is_directory) { - ApplyFolderPatchToFST(patch, folder, child, child_disc_patch, fst); + if (recursive) + { + ApplyFolderPatchToFST(patch, folder, + patch.m_file_data_loader->GetFolderContents(child_external_path), + child_external_path, recursive, child_disc_path, fst); + } } else { - DiscIO::FSTBuilderNode* node = FindFileNodeInFST(child_disc_patch, fst, folder.m_create); + DiscIO::FSTBuilderNode* node = FindFileNodeInFST(child_disc_path, fst, folder.m_create); if (node) - ApplyPatchToFile(patch, node, child.physicalName, 0, 0, folder.m_length, folder.m_resize); + ApplyPatchToFile(patch, node, child_external_path, 0, 0, folder.m_length, folder.m_resize); } } } static void ApplyUnknownFolderPatchToFST(const Patch& patch, const Folder& folder, - const ::File::FSTEntry& external_files, + const std::vector& external_files, + const std::string& external_path, bool recursive, std::vector* fst) { - for (const auto& child : external_files.children) + for (const auto& child : external_files) { - if (child.isDirectory) + const std::string child_external_path = external_path + "/" + child.m_filename; + if (child.m_is_directory) { - ApplyUnknownFolderPatchToFST(patch, folder, child, fst); + if (recursive) + { + ApplyUnknownFolderPatchToFST( + patch, folder, patch.m_file_data_loader->GetFolderContents(child_external_path), + child_external_path, recursive, fst); + } } else { std::vector nodes; - FindFilenameNodesInFST(&nodes, child.virtualName, fst); + FindFilenameNodesInFST(&nodes, child.m_filename, fst); for (auto* node : nodes) - ApplyPatchToFile(patch, node, child.physicalName, 0, 0, folder.m_length, folder.m_resize); + ApplyPatchToFile(patch, node, child_external_path, 0, 0, folder.m_length, folder.m_resize); } } } @@ -286,18 +454,24 @@ void ApplyPatchesToFiles(const std::vector& patches, for (const auto& folder : patch.m_folder_patches) { - ::File::FSTEntry external_files = - ::File::ScanDirectoryTree(patch.m_root + "/" + folder.m_external, folder.m_recursive); + const auto external_files = patch.m_file_data_loader->GetFolderContents(folder.m_external); std::string_view disc_path = folder.m_disc; while (StringBeginsWith(disc_path, "/")) disc_path.remove_prefix(1); while (StringEndsWith(disc_path, "/")) disc_path.remove_suffix(1); + if (disc_path.empty()) - ApplyUnknownFolderPatchToFST(patch, folder, external_files, fst); + { + ApplyUnknownFolderPatchToFST(patch, folder, external_files, folder.m_external, + folder.m_recursive, fst); + } else - ApplyFolderPatchToFST(patch, folder, external_files, disc_path, fst); + { + ApplyFolderPatchToFST(patch, folder, external_files, folder.m_external, folder.m_recursive, + disc_path, fst); + } } } @@ -339,19 +513,7 @@ static void ApplyMemoryPatch(u32 offset, const std::vector& value, static std::vector GetMemoryPatchValue(const Patch& patch, const Memory& memory_patch) { if (!memory_patch.m_valuefile.empty()) - { - ::File::IOFile f(patch.m_root + "/" + memory_patch.m_valuefile, "rb"); - if (!f) - return {}; - const u64 length = f.GetSize(); - std::vector value; - value.resize(length); - if (!f.ReadBytes(value.data(), length)) - return {}; - - return value; - } - + return patch.m_file_data_loader->GetFileContents(memory_patch.m_valuefile); return memory_patch.m_value; } -- cgit v1.2.3 From 588c31acb61d966dc05e0c1b346aad37deeb4905 Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Wed, 29 Sep 2021 07:49:32 +0200 Subject: RiivolutionPatcher: Ignore last two bits of offset in file patches to match console behavior. --- Source/Core/DiscIO/RiivolutionPatcher.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'Source/Core/DiscIO/RiivolutionPatcher.cpp') diff --git a/Source/Core/DiscIO/RiivolutionPatcher.cpp b/Source/Core/DiscIO/RiivolutionPatcher.cpp index 0af7ad93b7..af8be05944 100644 --- a/Source/Core/DiscIO/RiivolutionPatcher.cpp +++ b/Source/Core/DiscIO/RiivolutionPatcher.cpp @@ -298,7 +298,8 @@ static void ApplyPatchToFile(const Patch& patch, DiscIO::FSTBuilderNode* file_no static void ApplyPatchToFile(const Patch& patch, const File& file_patch, DiscIO::FSTBuilderNode* file_node) { - ApplyPatchToFile(patch, file_node, file_patch.m_external, file_patch.m_offset, + // The last two bits of the offset seem to be ignored by actual Riivolution. + ApplyPatchToFile(patch, file_node, file_patch.m_external, file_patch.m_offset & ~u64(3), file_patch.m_fileoffset, file_patch.m_length, file_patch.m_resize); } -- cgit v1.2.3 From fe242f79eeb5c92c39362203cdeb46e9b72707a7 Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Sun, 3 Oct 2021 06:23:33 +0200 Subject: Core: Implement Wii NAND path redirects for Riivolution savegame patches. --- Source/Core/DiscIO/RiivolutionPatcher.cpp | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) (limited to 'Source/Core/DiscIO/RiivolutionPatcher.cpp') diff --git a/Source/Core/DiscIO/RiivolutionPatcher.cpp b/Source/Core/DiscIO/RiivolutionPatcher.cpp index af8be05944..3f3a1f876f 100644 --- a/Source/Core/DiscIO/RiivolutionPatcher.cpp +++ b/Source/Core/DiscIO/RiivolutionPatcher.cpp @@ -14,6 +14,7 @@ #include "Common/IOFile.h" #include "Common/StringUtil.h" #include "Core/HW/Memmap.h" +#include "Core/IOS/FS/FileSystem.h" #include "Core/PowerPC/MMU.h" #include "DiscIO/DirectoryBlob.h" #include "DiscIO/RiivolutionParser.h" @@ -174,6 +175,12 @@ FileDataLoaderHostFS::MakeContentSource(std::string_view external_relative_path, ContentFile{std::move(*path), external_offset}}; } +std::optional +FileDataLoaderHostFS::ResolveSavegameRedirectPath(std::string_view external_relative_path) +{ + return MakeAbsoluteFromRelative(external_relative_path); +} + // 'before' and 'after' should be two copies of the same source // 'split_at' needs to be between the start and end of the source, may not match either boundary static void SplitAt(BuilderContentSource* before, BuilderContentSource* after, u64 split_at) @@ -588,4 +595,21 @@ void ApplyPatchesToMemory(const std::vector& patches) } } } + +std::optional +ExtractSavegameRedirect(const std::vector& riivolution_patches) +{ + for (const auto& patch : riivolution_patches) + { + if (!patch.m_savegame_patches.empty()) + { + const auto& save_patch = patch.m_savegame_patches[0]; + auto resolved = patch.m_file_data_loader->ResolveSavegameRedirectPath(save_patch.m_external); + if (resolved) + return SavegameRedirect{std::move(*resolved), save_patch.m_clone}; + return std::nullopt; + } + } + return std::nullopt; +} } // namespace DiscIO::Riivolution -- cgit v1.2.3 From ba3373b47623677942a0c8db8165eb62f820c39e Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Thu, 21 Oct 2021 05:03:00 +0200 Subject: RiivolutionPatcher: Modify memory patching logic to be more accurate to actual Riivolution. --- Source/Core/DiscIO/RiivolutionPatcher.cpp | 60 +++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 18 deletions(-) (limited to 'Source/Core/DiscIO/RiivolutionPatcher.cpp') diff --git a/Source/Core/DiscIO/RiivolutionPatcher.cpp b/Source/Core/DiscIO/RiivolutionPatcher.cpp index 3f3a1f876f..aefa4950b9 100644 --- a/Source/Core/DiscIO/RiivolutionPatcher.cpp +++ b/Source/Core/DiscIO/RiivolutionPatcher.cpp @@ -527,20 +527,23 @@ static std::vector GetMemoryPatchValue(const Patch& patch, const Memory& mem static void ApplyMemoryPatch(const Patch& patch, const Memory& memory_patch) { + if (memory_patch.m_offset == 0) + return; + ApplyMemoryPatch(memory_patch.m_offset | 0x80000000, GetMemoryPatchValue(patch, memory_patch), memory_patch.m_original); } -static void ApplySearchMemoryPatch(const Patch& patch, const Memory& memory_patch) +static void ApplySearchMemoryPatch(const Patch& patch, const Memory& memory_patch, u32 ram_start, + u32 length) { - if (memory_patch.m_original.empty()) + if (memory_patch.m_original.empty() || memory_patch.m_align == 0) return; - const u32 ram_size = ::Memory::GetRamSize(); - const u32 stride = memory_patch.m_align < 1 ? 1 : memory_patch.m_align; - for (u32 i = 0; i < ram_size - (stride - 1); i += stride) + const u32 stride = memory_patch.m_align; + for (u32 i = 0; i < length - (stride - 1); i += stride) { - const u32 address = i | 0x80000000; + const u32 address = ram_start + i; if (MemoryMatchesAt(address, memory_patch.m_original)) { ApplyMemoryPatch(address, GetMemoryPatchValue(patch, memory_patch), {}); @@ -549,22 +552,25 @@ static void ApplySearchMemoryPatch(const Patch& patch, const Memory& memory_patc } } -static void ApplyOcarinaMemoryPatch(const Patch& patch, const Memory& memory_patch) +static void ApplyOcarinaMemoryPatch(const Patch& patch, const Memory& memory_patch, u32 ram_start, + u32 length) { - if (memory_patch.m_value.empty()) + if (memory_patch.m_offset == 0) + return; + const std::vector value = GetMemoryPatchValue(patch, memory_patch); + if (value.empty()) return; - const u32 ram_size = ::Memory::GetRamSize(); - for (u32 i = 0; i < ram_size; i += 4) + for (u32 i = 0; i < length; i += 4) { // first find the pattern - const u32 address = i | 0x80000000; - if (MemoryMatchesAt(address, memory_patch.m_value)) + const u32 address = ram_start + i; + if (MemoryMatchesAt(address, value)) { - for (; i < ram_size; i += 4) + for (; i < length; i += 4) { // from the pattern find the next blr instruction - const u32 blr_address = i | 0x80000000; + const u32 blr_address = ram_start + i; auto blr = PowerPC::HostTryReadU32(blr_address); if (blr && blr->value == 0x4e800020) { @@ -580,22 +586,40 @@ static void ApplyOcarinaMemoryPatch(const Patch& patch, const Memory& memory_pat } } -void ApplyPatchesToMemory(const std::vector& patches) +void ApplyGeneralMemoryPatches(const std::vector& patches) { for (const auto& patch : patches) { for (const auto& memory : patch.m_memory_patches) { if (memory.m_ocarina) - ApplyOcarinaMemoryPatch(patch, memory); - else if (memory.m_search) - ApplySearchMemoryPatch(patch, memory); + continue; + + if (memory.m_search) + ApplySearchMemoryPatch(patch, memory, 0x80000000, ::Memory::GetRamSize()); else ApplyMemoryPatch(patch, memory); } } } +void ApplyApploaderMemoryPatches(const std::vector& patches, u32 ram_address, u32 ram_length) +{ + for (const auto& patch : patches) + { + for (const auto& memory : patch.m_memory_patches) + { + if (!memory.m_ocarina && !memory.m_search) + continue; + + if (memory.m_ocarina) + ApplyOcarinaMemoryPatch(patch, memory, ram_address, ram_length); + else + ApplySearchMemoryPatch(patch, memory, ram_address, ram_length); + } + } +} + std::optional ExtractSavegameRedirect(const std::vector& riivolution_patches) { -- cgit v1.2.3 From 22c6f2fe3bd14e46b160a524c2ddccecd9bb96c9 Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Fri, 22 Oct 2021 03:14:51 +0200 Subject: RiivolutionPatcher: Handle the possibility of the FST already containing a main.dol file. --- Source/Core/DiscIO/RiivolutionPatcher.cpp | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) (limited to 'Source/Core/DiscIO/RiivolutionPatcher.cpp') diff --git a/Source/Core/DiscIO/RiivolutionPatcher.cpp b/Source/Core/DiscIO/RiivolutionPatcher.cpp index aefa4950b9..8f1b88caf3 100644 --- a/Source/Core/DiscIO/RiivolutionPatcher.cpp +++ b/Source/Core/DiscIO/RiivolutionPatcher.cpp @@ -435,8 +435,10 @@ void ApplyPatchesToFiles(const std::vector& patches, // For file searching purposes, Riivolution assumes that the game's main.dol is in the root of the // file system. So to avoid doing a bunch of special case handling for that, we just put a node // for this into the FST and remove it again after the file patching is done. - dol_node->m_filename = "main.dol"; - fst->push_back(*dol_node); + // We mark the inserted node with a pointer to a stack variable so we can find it again. + int marker = 0; + fst->emplace_back(DiscIO::FSTBuilderNode{"main.dol", dol_node->m_size, + std::move(dol_node->m_content), &marker}); for (const auto& patch : patches) { @@ -483,15 +485,21 @@ void ApplyPatchesToFiles(const std::vector& patches, } } + // Remove the inserted main.dol node again and propagate its changes. auto main_dol_node_in_fst = - std::find_if(fst->begin(), fst->end(), [&](const DiscIO::FSTBuilderNode& node) { - return node.m_filename == "main.dol"; - }); + std::find_if(fst->begin(), fst->end(), + [&](const DiscIO::FSTBuilderNode& node) { return node.m_user_data == ▮ }); if (main_dol_node_in_fst != fst->end()) { - *dol_node = *main_dol_node_in_fst; + dol_node->m_size = main_dol_node_in_fst->m_size; + dol_node->m_content = std::move(main_dol_node_in_fst->m_content); fst->erase(main_dol_node_in_fst); } + else + { + // The main.dol node disappeared, this should never happen. + ASSERT(false); + } } static bool MemoryMatchesAt(u32 offset, const std::vector& value) -- cgit v1.2.3 From fe7f3a6f26a1e9fc14e97d98d8db1aa07819861d Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Fri, 22 Oct 2021 06:27:37 +0200 Subject: RiivolutionPatcher: Protect against three or more dots in path traversal. --- Source/Core/DiscIO/RiivolutionPatcher.cpp | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'Source/Core/DiscIO/RiivolutionPatcher.cpp') diff --git a/Source/Core/DiscIO/RiivolutionPatcher.cpp b/Source/Core/DiscIO/RiivolutionPatcher.cpp index 8f1b88caf3..2e400507e1 100644 --- a/Source/Core/DiscIO/RiivolutionPatcher.cpp +++ b/Source/Core/DiscIO/RiivolutionPatcher.cpp @@ -98,6 +98,13 @@ FileDataLoaderHostFS::MakeAbsoluteFromRelative(std::string_view external_relativ result.pop_back(); result.pop_back(); } + else if (std::all_of(element.begin(), element.end(), [](char c) { return c == '.'; })) + { + // This is a triple, quadruple, etc. dot. + // Some file systems treat this as several 'up' path traversals, but Riivolution does not. + // If someone tries this just error out, it wouldn't work sensibly in Riivolution anyway. + return std::nullopt; + } else { // We're going down a level. -- cgit v1.2.3 From 4f8281084598d2dc3bf2e15abd84d9c490e033c9 Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Sat, 23 Oct 2021 17:06:55 +0200 Subject: RiivolutionPatcher: Use FileInfo instead of IOFile when only checking for filesize. --- Source/Core/DiscIO/RiivolutionPatcher.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'Source/Core/DiscIO/RiivolutionPatcher.cpp') diff --git a/Source/Core/DiscIO/RiivolutionPatcher.cpp b/Source/Core/DiscIO/RiivolutionPatcher.cpp index 2e400507e1..eb8f55f45a 100644 --- a/Source/Core/DiscIO/RiivolutionPatcher.cpp +++ b/Source/Core/DiscIO/RiivolutionPatcher.cpp @@ -135,8 +135,8 @@ FileDataLoaderHostFS::GetExternalFileSize(std::string_view external_relative_pat auto path = MakeAbsoluteFromRelative(external_relative_path); if (!path) return std::nullopt; - ::File::IOFile f(*path, "rb"); - if (!f) + ::File::FileInfo f(*path); + if (!f.IsFile()) return std::nullopt; return f.GetSize(); } -- cgit v1.2.3