diff --git a/src/core/file_sys/ips_layer.cpp b/src/core/file_sys/ips_layer.cpp index df933ee367..6bce138a82 100644 --- a/src/core/file_sys/ips_layer.cpp +++ b/src/core/file_sys/ips_layer.cpp @@ -2,7 +2,9 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include #include "common/assert.h" +#include "common/hex_util.h" #include "common/swap.h" #include "core/file_sys/ips_layer.h" #include "core/file_sys/vfs_vector.h" @@ -85,4 +87,144 @@ VirtualFile PatchIPS(const VirtualFile& in, const VirtualFile& ips) { return std::make_shared(in_data, in->GetName(), in->GetContainingDirectory()); } +IPSwitchCompiler::IPSwitchCompiler(VirtualFile patch_text_) + : valid(false), patch_text(std::move(patch_text_)), nso_build_id{}, is_little_endian(false), + offset_shift(0), print_values(false) { + Parse(); +} + +std::array IPSwitchCompiler::GetBuildID() const { + return nso_build_id; +} + +bool IPSwitchCompiler::IsValid() const { + return valid; +} + +static bool StartsWith(const std::string& base, const std::string& check) { + return base.size() >= check.size() && base.substr(0, check.size()) == check; +} + +void IPSwitchCompiler::Parse() { + const auto bytes = patch_text->ReadAllBytes(); + std::stringstream s; + s.write(reinterpret_cast(bytes.data()), bytes.size()); + + std::vector lines; + std::string line; + while (std::getline(s, line)) { + // Remove a trailing \r + if (!line.empty() && line[line.size() - 1] == '\r') + line = line.substr(0, line.size() - 1); + lines.push_back(line); + } + + for (std::size_t i = 0; i < lines.size(); ++i) { + auto line = lines[i]; + if (StartsWith(line, "@stop")) { + // Force stop + break; + } else if (StartsWith(line, "@nsobid-")) { + // NSO Build ID Specifier + auto raw_build_id = line.substr(8); + if (raw_build_id.size() != 0x40) + raw_build_id.resize(0x40, '0'); + nso_build_id = Common::HexStringToArray<0x20>(raw_build_id); + } else if (StartsWith(line, "@flag offset_shift ")) { + // Offset Shift Flag + offset_shift = std::stoull(line.substr(19), nullptr, 0); + } else if (StartsWith(line, "#")) { + // Mandatory Comment + LOG_INFO(Loader, "[IPSwitchCompiler ('{}')] Forced output comment: {}", + patch_text->GetName(), line.substr(1)); + } else if (StartsWith(line, "@little-endian")) { + // Set values to read as little endian + is_little_endian = true; + } else if (StartsWith(line, "@big-endian")) { + // Set values to read as big endian + is_little_endian = false; + } else if (StartsWith(line, "@flag print_values")) { + // Force printing of applied values + print_values = true; + } else if (StartsWith(line, "@enabled") || StartsWith(line, "@disabled")) { + // Start of patch + const auto enabled = StartsWith(line, "@enabled"); + if (i == 0) + return; + const auto name = lines[i - 1].substr(3); + LOG_INFO(Loader, "[IPSwitchCompiler ('{}')] Parsing patch '{}' ({})", + patch_text->GetName(), name, line.substr(1)); + + IPSwitchPatch patch{name, enabled, {}}; + + // Read rest of patch + while (true) { + if (i + 1 >= lines.size()) + break; + line = lines[++i]; + + // 11 - 8 hex digit offset + space + minimum two digit overwrite val + if (line.length() < 11) + break; + auto offset = std::stoul(line.substr(0, 8), nullptr, 16); + offset += offset_shift; + + std::vector replace; + // 9 - first char of replacement val + if (line[9] == '\"') { + // string replacement + const auto end_index = line.find_last_of('\"'); + if (end_index == std::string::npos || end_index < 10) + return; + const auto value = line.substr(10, end_index - 10); + replace.reserve(value.size()); + std::copy(value.begin(), value.end(), std::back_inserter(replace)); + } else { + // hex replacement + const auto value = line.substr(9); + replace.reserve(value.size() / 2); + replace = Common::HexStringToVector(value, is_little_endian); + } + + if (print_values) { + LOG_INFO(Loader, + "[IPSwitchCompiler ('{}')] - Patching value at offset 0x{:08X} " + "with byte string '{}'", + patch_text->GetName(), offset, Common::HexVectorToString(replace)); + } + + patch.records.emplace(offset, replace); + } + + patches.push_back(std::move(patch)); + } + } + + valid = true; +} + +VirtualFile IPSwitchCompiler::Apply(const VirtualFile& in) const { + if (in == nullptr || !valid) + return nullptr; + + auto in_data = in->ReadAllBytes(); + + for (const auto& patch : patches) { + if (!patch.enabled) + continue; + + for (const auto& record : patch.records) { + if (record.first >= in_data.size()) + continue; + auto replace_size = record.second.size(); + if (record.first + replace_size > in_data.size()) + replace_size = in_data.size() - record.first; + for (std::size_t i = 0; i < replace_size; ++i) + in_data[i + record.first] = record.second[i]; + } + } + + return std::make_shared(in_data, in->GetName(), in->GetContainingDirectory()); +} + } // namespace FileSys diff --git a/src/core/file_sys/ips_layer.h b/src/core/file_sys/ips_layer.h index 81c1634945..bb35542c81 100644 --- a/src/core/file_sys/ips_layer.h +++ b/src/core/file_sys/ips_layer.h @@ -12,4 +12,30 @@ namespace FileSys { VirtualFile PatchIPS(const VirtualFile& in, const VirtualFile& ips); +class IPSwitchCompiler { +public: + explicit IPSwitchCompiler(VirtualFile patch_text); + std::array GetBuildID() const; + bool IsValid() const; + VirtualFile Apply(const VirtualFile& in) const; + +private: + void Parse(); + + bool valid; + + struct IPSwitchPatch { + std::string name; + bool enabled; + std::map> records; + }; + + VirtualFile patch_text; + std::vector patches; + std::array nso_build_id; + bool is_little_endian; + u64 offset_shift; + bool print_values; +}; + } // namespace FileSys