[file_sys] robust-er IPSwitch compiler (#3911)

this PR reworks IPS parser to be less stupid
what do i mean by this? well generally give it a bit of
love so it doesn't do a lot of unsound allocations
also simplify the logic greatly (and use memcmp() string idiom
instead of trusting the compiler so blindly...)

no this doesn't mean to uber optimize IPS to handle 999 gb/s
it's more so it doesn't outright crash with edge cases
as the previous codebase was quite spaghetty

also the major overhead is obviously the vector shenanigans
and the I/O -- but thats out of scope

Test that IPSwitch mods still properly work WITH ANY GAME
IF THERE IS ANY REGRESSION IN SOME GAME/MOD THEN
TELL ME

Signed-off-by: lizzie <lizzie@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3911
Reviewed-by: crueter <crueter@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
This commit is contained in:
lizzie
2026-08-30 08:10:51 +02:00
committed by crueter
parent 28ab4a1a01
commit 672bcbae01
4 changed files with 170 additions and 253 deletions
+160 -233
View File
@@ -3,10 +3,12 @@
#include <algorithm> #include <algorithm>
#include <cstring> #include <cstring>
#include <map>
#include <sstream> #include <sstream>
#include <string> #include <string>
#include <utility> #include <utility>
#include <span>
#include <cctype>
#include <ankerl/unordered_dense.h>
#include "common/hex_util.h" #include "common/hex_util.h"
#include "common/logging.h" #include "common/logging.h"
@@ -22,61 +24,30 @@ enum class IPSFileType {
Error, Error,
}; };
constexpr std::array<std::pair<const char*, const char*>, 11> ESCAPE_CHARACTER_MAP{{ static IPSFileType IdentifyMagic(std::span<const u8> magic) {
{"\\a", "\a"}, if (magic.size() >= 5) {
{"\\b", "\b"}, if (std::memcmp(magic.data(), "PATCH", 5) == 0)
{"\\f", "\f"}, return IPSFileType::IPS;
{"\\n", "\n"}, if (std::memcmp(magic.data(), "IPS32", 5) == 0)
{"\\r", "\r"}, return IPSFileType::IPS32;
{"\\t", "\t"},
{"\\v", "\v"},
{"\\\\", "\\"},
{"\\\'", "\'"},
{"\\\"", "\""},
{"\\\?", "\?"},
}};
static IPSFileType IdentifyMagic(const std::vector<u8>& magic) {
if (magic.size() != 5) {
return IPSFileType::Error;
} }
static constexpr std::array<u8, 5> patch_magic{{'P', 'A', 'T', 'C', 'H'}};
if (std::equal(magic.begin(), magic.end(), patch_magic.begin())) {
return IPSFileType::IPS;
}
static constexpr std::array<u8, 5> ips32_magic{{'I', 'P', 'S', '3', '2'}};
if (std::equal(magic.begin(), magic.end(), ips32_magic.begin())) {
return IPSFileType::IPS32;
}
return IPSFileType::Error; return IPSFileType::Error;
} }
static bool IsEOF(IPSFileType type, const std::vector<u8>& data) { static bool IsEOF(IPSFileType type, std::span<const u8> magic) {
static constexpr std::array<u8, 3> eof{{'E', 'O', 'F'}}; return (type == IPSFileType::IPS && magic.size() > 3 && std::memcmp(magic.data(), "EOF", 3) == 0)
if (type == IPSFileType::IPS && std::equal(data.begin(), data.end(), eof.begin())) { || (type == IPSFileType::IPS32 && magic.size() > 4 && std::memcmp(magic.data(), "EEOF", 4) == 0);
return true;
}
static constexpr std::array<u8, 4> eeof{{'E', 'E', 'O', 'F'}};
return type == IPSFileType::IPS32 && std::equal(data.begin(), data.end(), eeof.begin());
} }
VirtualFile PatchIPS(const VirtualFile& in, const VirtualFile& ips) { VirtualFile PatchIPS(const VirtualFile& in, const VirtualFile& ips) {
if (in == nullptr || ips == nullptr) if (in == nullptr || ips == nullptr)
return nullptr; return nullptr;
const auto type = IdentifyMagic(ips->ReadBytes(0x5)); auto in_data = in->ReadAllBytes();
auto const type = IdentifyMagic(in_data);
if (type == IPSFileType::Error) if (type == IPSFileType::Error)
return nullptr; return nullptr;
auto in_data = in->ReadAllBytes();
if (in_data.size() == 0) {
return nullptr;
}
std::vector<u8> temp(type == IPSFileType::IPS ? 3 : 4); std::vector<u8> temp(type == IPSFileType::IPS ? 3 : 4);
u64 offset = 5; // After header u64 offset = 5; // After header
while (ips->Read(temp.data(), temp.size(), offset) == temp.size()) { while (ips->Read(temp.data(), temp.size(), offset) == temp.size()) {
@@ -85,12 +56,9 @@ VirtualFile PatchIPS(const VirtualFile& in, const VirtualFile& ips) {
break; break;
} }
u32 real_offset{}; u32 real_offset = (type == IPSFileType::IPS32)
if (type == IPSFileType::IPS32) ? ((temp[0] << 24) | (temp[1] << 16) | (temp[2] << 8) | temp[3])
real_offset = (temp[0] << 24) | (temp[1] << 16) | (temp[2] << 8) | temp[3]; : ((temp[0] << 16) | (temp[1] << 8) | temp[2]);
else
real_offset = (temp[0] << 16) | (temp[1] << 8) | temp[2];
if (real_offset > in_data.size()) { if (real_offset > in_data.size()) {
return nullptr; return nullptr;
} }
@@ -113,34 +81,35 @@ VirtualFile PatchIPS(const VirtualFile& in, const VirtualFile& ips) {
return nullptr; return nullptr;
if (real_offset + rle_size > in_data.size()) if (real_offset + rle_size > in_data.size())
rle_size = static_cast<u16>(in_data.size() - real_offset); rle_size = u16(in_data.size() - real_offset);
std::memset(in_data.data() + real_offset, *data, rle_size); std::memset(in_data.data() + real_offset, *data, rle_size);
} else { // Standard Patch } else { // Standard Patch
auto read = data_size; auto read = data_size;
if (real_offset + read > in_data.size()) if (real_offset + read > in_data.size())
read = static_cast<u16>(in_data.size() - real_offset); read = u16(in_data.size() - real_offset);
if (ips->Read(in_data.data() + real_offset, read, offset) != data_size) if (ips->Read(in_data.data() + real_offset, read, offset) != data_size)
return nullptr; return nullptr;
offset += data_size; offset += data_size;
} }
} }
if (IsEOF(type, temp)) {
if (!IsEOF(type, temp)) { return std::make_shared<VectorVfsFile>(std::move(in_data), in->GetName(), in->GetContainingDirectory());
return nullptr;
} }
return nullptr;
return std::make_shared<VectorVfsFile>(std::move(in_data), in->GetName(),
in->GetContainingDirectory());
} }
struct IPSwitchRecord {
std::array<uint8_t, 256 - sizeof(size_t)> data;
size_t count;
};
struct IPSwitchCompiler::IPSwitchPatch { struct IPSwitchCompiler::IPSwitchPatch {
std::string name; ankerl::unordered_dense::map<u32, IPSwitchRecord> records;
bool enabled; bool enabled;
std::map<u32, std::vector<u8>> records;
}; };
IPSwitchCompiler::IPSwitchCompiler(VirtualFile patch_text_) : patch_text(std::move(patch_text_)) { IPSwitchCompiler::IPSwitchCompiler(VirtualFile patch_text_) : patch_text(std::move(patch_text_)) {
Parse(); Parse(patch_text->ReadAllBytes());
} }
IPSwitchCompiler::~IPSwitchCompiler() = default; IPSwitchCompiler::~IPSwitchCompiler() = default;
@@ -149,201 +118,159 @@ std::array<u8, 32> IPSwitchCompiler::GetBuildID() const {
return nso_build_id; return nso_build_id;
} }
bool IPSwitchCompiler::IsValid() const { static IPSwitchRecord EscapeStringSequences(std::string_view sv) {
return valid; IPSwitchRecord r{};
} for (auto it = sv.cbegin(); it != sv.cend(); ) {
if (*it == '\\' && it + 1 < sv.cend()) {
static bool StartsWith(std::string_view base, std::string_view check) { switch (it[1]) {
return base.size() >= check.size() && base.substr(0, check.size()) == check; case 'n': r.data[r.count] = '\n'; break;
} case 't': r.data[r.count] = '\t'; break;
case 'b': r.data[r.count] = '\b'; break;
static std::string EscapeStringSequences(std::string in) { case 'r': r.data[r.count] = '\r'; break;
for (const auto& seq : ESCAPE_CHARACTER_MAP) { case 'e': r.data[r.count] = '\e'; break;
for (auto index = in.find(seq.first); index != std::string::npos; case 'v': r.data[r.count] = '\v'; break;
index = in.find(seq.first, index)) { case '?': r.data[r.count] = '\?'; break;
in.replace(index, std::strlen(seq.first), seq.second); default: r.data[r.count] = it[1]; break;
index += std::strlen(seq.second); }
++r.count;
it += 2;
} else {
++r.count;
++it;
} }
} }
return r;
return in;
} }
void IPSwitchCompiler::ParseFlag(const std::string& line) { [[nodiscard]] static inline std::array<u8, 32> ReadNSOBuildId(std::string_view const s) {
if (StartsWith(line, "@flag offset_shift ")) { std::array<u8, 32> r{};
// Offset Shift Flag for (std::size_t i = 0; i < s.size(); ++i)
offset_shift = std::strtoll(line.substr(19).c_str(), nullptr, 0); r[i / 2] |= u8(u8(Common::ToHexNibble(s[i])) << u8((i % 2) * 4));
} else if (StartsWith(line, "@little-endian")) { return r;
// 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;
}
} }
void IPSwitchCompiler::Parse() { void IPSwitchCompiler::Parse(std::span<u8 const> bytes) {
const auto bytes = patch_text->ReadAllBytes(); LOG_INFO(Loader, "IPSwitchCompiler: '{}'", patch_text->GetName());
std::stringstream s; bool is_little_endian = false;
s.write(reinterpret_cast<const char*>(bytes.data()), bytes.size()); s64 offset_shift = 0;
//bool print_values = false;
std::vector<std::string> lines; auto const parse_line = [&](std::string_view const line) {
std::string stream_line; // Keep in mind lines have trimmed spaces (at the end & start)!
while (std::getline(s, stream_line)) { LOG_INFO(Loader, "<{}>", line);
// Remove a trailing \r if (line.starts_with("@stop")) {
if (!stream_line.empty() && stream_line.back() == '\r') return false; // Force stop
stream_line.pop_back(); } else if (line.starts_with("@nsobid-")) { // NSO Build ID Specifier
lines.push_back(std::move(stream_line)); nso_build_id = ReadNSOBuildId(line.substr(8));
} } else if (line.starts_with("@enabled")) {
patches.push_back({{}, true}); //enabled patch
} else if (line.starts_with("@disabled")) {
patches.push_back({{}, false}); //disabled patch
} else if (line.starts_with("@flag offset_shift ")) {
offset_shift = std::strtoll(line.data() + 19, nullptr, 0); // Offset Shift Flag
} else if (line.starts_with("@little-endian")) {
is_little_endian = true; // Set values to read as little endian
} else if (line.starts_with("@big-endian")) {
is_little_endian = false; // Set values to read as big endian
} else if (line.starts_with("@flag print_values")) {
//print_values = true; // Force printing of applied values
} else if (line.starts_with("@")) {
LOG_WARNING(Loader, "Unknown flag {}", line);
} else {
size_t offset = size_t(std::strtoul(line.data(), nullptr, 16));
offset += size_t(offset_shift);
if (auto const first_quote = line.find_first_of("\"\'"); first_quote != std::string::npos) {
// string replacement
char quote = line[first_quote];
auto const start = line.cbegin() + first_quote + 1;
auto end = start;
for (; end < line.cend() && *end != quote; )
end += (*end == '\\') ? 2 : 1;
if (start <= line.cend() && end <= line.cend()) {
LOG_INFO(Loader, "[S] value @ {:#08X} ", offset);
patches.back().records.insert_or_assign(u32(offset), EscapeStringSequences({start, end}));
} else {
LOG_WARNING(Loader, "invalid string");
}
} else if (auto const first_space = line.find_last_of(" /\t\r\n"); first_space != std::string::npos) {
IPSwitchRecord r{}; // hex replacement
auto const start = line.cbegin() + first_space + 1;
auto const end = line.cend();
if (start <= line.cend() && end <= line.cend()) {
auto const hs = Common::HexStringToVector({start, end}, is_little_endian);
std::memcpy(r.data.data(), hs.data(), hs.size());
r.count = hs.size();
LOG_INFO(Loader, "[H] value @ {:#08X}", offset);
patches.back().records.insert_or_assign(u32(offset), std::move(r));
} else {
LOG_WARNING(Loader, "invalid line");
}
} else {
LOG_WARNING(Loader, "unhandled line!");
}
}
return true; //continue
};
for (std::size_t i = 0; i < lines.size(); ++i) { for (auto it = bytes.begin(); it < bytes.end(); ) {
auto line = lines[i]; auto const start = it;
auto end = start;
// Remove midline comments for (; end < bytes.end() && *end != '\n' && *end != '\r'; ++end)
std::size_t comment_index = std::string::npos; ;
bool within_string = false; it = end + 1; //prepare for next line
for (std::size_t k = 0; k < line.size(); ++k) { std::string_view const sline{
if (line[k] == '\"' && (k > 0 && line[k - 1] != '\\')) { reinterpret_cast<const char*>(bytes.data() + std::distance(bytes.begin(), start)),
within_string = !within_string; size_t(std::distance(start, end))
} else if (line[k] == '\\' && (k < line.size() - 1 && line[k + 1] == '\\')) { };
comment_index = k; if (sline.size() > 0) {
auto p = sline.cbegin();
// skip space off line
for (; p < sline.cend() && std::isspace(*p); ++p)
;
// now make a nominal preprocessed line: remove comments
char quote = '\0';
auto const sline_start = p;
for (; p < sline.cend(); ) {
if ((!quote && p + 1 < sline.cend() && p[0] == '/' && p[1] == '/')
|| (!quote && p[0] == '#')) {
break;
} else if (p[0] == '\"' || p[0] == '\'') {
quote = (p[0] == quote) ? '\0' : p[0];
++p;
} else if (p + 1 < sline.cend() && p[0] == '\\') {
p += 2;
} else {
++p;
}
}
// now we have the preprocessed string ;)
std::string_view pp_str(sline_start, p);
if (pp_str.size() > 0 && !parse_line(pp_str)) {
break; break;
} }
} }
if (!StartsWith(line, "//") && comment_index != std::string::npos) {
last_comment = line.substr(comment_index + 2);
line = line.substr(0, comment_index);
}
if (StartsWith(line, "@stop")) {
// Force stop
break;
} else if (StartsWith(line, "@nsobid-")) {
// NSO Build ID Specifier
const auto raw_build_id = fmt::format("{:0<64}", line.substr(8));
nso_build_id = Common::HexStringToArray<0x20>(raw_build_id);
} else if (StartsWith(line, "#")) {
// Mandatory Comment
LOG_INFO(Loader, "[IPSwitchCompiler ('{}')] Forced output comment: {}",
patch_text->GetName(), line.substr(1));
} else if (StartsWith(line, "//")) {
// Normal Comment
last_comment = line.substr(2);
if (last_comment.find_first_not_of(' ') == std::string::npos)
continue;
if (last_comment.find_first_not_of(' ') != 0)
last_comment = last_comment.substr(last_comment.find_first_not_of(' '));
} else if (StartsWith(line, "@enabled") || StartsWith(line, "@disabled")) {
// Start of patch
const auto enabled = StartsWith(line, "@enabled");
if (i == 0)
return;
LOG_INFO(Loader, "[IPSwitchCompiler ('{}')] Parsing patch '{}' ({})",
patch_text->GetName(), last_comment, line.substr(1));
IPSwitchPatch patch{last_comment, enabled, {}};
// Read rest of patch
while (true) {
if (i + 1 >= lines.size()) {
break;
}
const auto& patch_line = lines[++i];
// Patch line may contain comments
if (StartsWith(patch_line, "//") || StartsWith(patch_line, "#")) {
continue;
}
// Start of new patch
if (StartsWith(patch_line, "@enabled") || StartsWith(patch_line, "@disabled")) {
--i;
break;
}
// Check for a flag
if (StartsWith(patch_line, "@")) {
ParseFlag(patch_line);
continue;
}
// 11 - 8 hex digit offset + space + minimum two digit overwrite val
if (patch_line.length() < 11)
break;
auto offset = std::strtoul(patch_line.substr(0, 8).c_str(), nullptr, 16);
offset += static_cast<unsigned long>(offset_shift);
std::vector<u8> replace;
// 9 - first char of replacement val
if (patch_line[9] == '\"') {
// string replacement
auto end_index = patch_line.find('\"', 10);
if (end_index == std::string::npos || end_index < 10)
return;
while (patch_line[end_index - 1] == '\\') {
end_index = patch_line.find('\"', end_index + 1);
if (end_index == std::string::npos || end_index < 10)
return;
}
auto value = patch_line.substr(10, end_index - 10);
value = EscapeStringSequences(value);
replace.reserve(value.size());
std::copy(value.begin(), value.end(), std::back_inserter(replace));
} else {
// hex replacement
const auto value =
patch_line.substr(9, patch_line.find_first_of(" /\r\n", 9) - 9);
replace = Common::HexStringToVector(value, is_little_endian);
}
if (print_values) {
LOG_INFO(Loader,
"[IPSwitchCompiler ('{}')] - Patching value at offset {:#08x} "
"with byte string '{}'",
patch_text->GetName(), offset, Common::HexToString(replace));
}
patch.records.insert_or_assign(static_cast<u32>(offset), std::move(replace));
}
patches.push_back(std::move(patch));
} else if (StartsWith(line, "@")) {
ParseFlag(line);
}
} }
valid = true;
} }
VirtualFile IPSwitchCompiler::Apply(const VirtualFile& in) const { VirtualFile IPSwitchCompiler::Apply(const VirtualFile& in) const {
if (in == nullptr || !valid) if (in == nullptr)
return nullptr; return nullptr;
auto in_data = in->ReadAllBytes(); auto in_data = in->ReadAllBytes();
for (const auto& patch : patches) { for (const auto& patch : patches) {
if (!patch.enabled) if (patch.enabled) {
continue; for (const auto& record : patch.records) {
if (record.first < in_data.size()) {
for (const auto& record : patch.records) { auto replace_size = record.second.count;
if (record.first >= in_data.size()) if (record.first + replace_size > in_data.size())
continue; replace_size = in_data.size() - record.first;
auto replace_size = record.second.size(); std::memcpy(in_data.data() + record.first, record.second.data.data(), replace_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<VectorVfsFile>(std::move(in_data), in->GetName(), in->GetContainingDirectory());
return std::make_shared<VectorVfsFile>(std::move(in_data), in->GetName(),
in->GetContainingDirectory());
} }
} // namespace FileSys } // namespace FileSys
+5 -9
View File
@@ -1,11 +1,14 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#pragma once #pragma once
#include <array> #include <array>
#include <memory>
#include <vector> #include <vector>
#include <span>
#include "common/common_types.h" #include "common/common_types.h"
#include "core/file_sys/vfs/vfs.h" #include "core/file_sys/vfs/vfs.h"
@@ -20,24 +23,17 @@ public:
~IPSwitchCompiler(); ~IPSwitchCompiler();
std::array<u8, 0x20> GetBuildID() const; std::array<u8, 0x20> GetBuildID() const;
bool IsValid() const;
VirtualFile Apply(const VirtualFile& in) const; VirtualFile Apply(const VirtualFile& in) const;
private: private:
struct IPSwitchPatch; struct IPSwitchPatch;
void ParseFlag(const std::string& flag); void ParseFlag(const std::string& flag);
void Parse(); void Parse(std::span<u8 const> bytes);
bool valid = false;
VirtualFile patch_text; VirtualFile patch_text;
std::vector<IPSwitchPatch> patches; std::vector<IPSwitchPatch> patches;
std::array<u8, 0x20> nso_build_id{}; std::array<u8, 0x20> nso_build_id{};
bool is_little_endian = false;
s64 offset_shift = 0;
bool print_values = false;
std::string last_comment = "";
}; };
} // namespace FileSys } // namespace FileSys
+2 -9
View File
@@ -345,8 +345,7 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const {
return exefs; return exefs;
} }
std::vector<VirtualFile> PatchManager::CollectPatches(const std::vector<VirtualDir>& patch_dirs, std::vector<VirtualFile> PatchManager::CollectPatches(const std::vector<VirtualDir>& patch_dirs, const std::string& build_id) const {
const std::string& build_id) const {
const auto& disabled = Settings::values.disabled_addons[title_id]; const auto& disabled = Settings::values.disabled_addons[title_id];
const auto nso_build_id = fmt::format("{:0<64}", build_id); const auto nso_build_id = fmt::format("{:0<64}", build_id);
@@ -361,16 +360,11 @@ std::vector<VirtualFile> PatchManager::CollectPatches(const std::vector<VirtualD
for (const auto& file : exefs_dir->GetFiles()) { for (const auto& file : exefs_dir->GetFiles()) {
if (file->GetExtension() == "ips") { if (file->GetExtension() == "ips") {
auto name = file->GetName(); auto name = file->GetName();
const auto this_build_id = fmt::format("{:0<64}", name.substr(0, name.find('.')));
const auto this_build_id =
fmt::format("{:0<64}", name.substr(0, name.find('.')));
if (nso_build_id == this_build_id) if (nso_build_id == this_build_id)
out.push_back(file); out.push_back(file);
} else if (file->GetExtension() == "pchtxt") { } else if (file->GetExtension() == "pchtxt") {
IPSwitchCompiler compiler{file}; IPSwitchCompiler compiler{file};
if (!compiler.IsValid())
continue;
const auto this_build_id = Common::HexToString(compiler.GetBuildID()); const auto this_build_id = Common::HexToString(compiler.GetBuildID());
if (nso_build_id == this_build_id) if (nso_build_id == this_build_id)
out.push_back(file); out.push_back(file);
@@ -378,7 +372,6 @@ std::vector<VirtualFile> PatchManager::CollectPatches(const std::vector<VirtualD
} }
} }
} }
return out; return out;
} }
+3 -2
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
#include <filesystem> #include <filesystem>
#include <system_error>
#include <JlCompress.h> #include <JlCompress.h>
#include "frontend_common/mod_manager.h" #include "frontend_common/mod_manager.h"
#include "mod.h" #include "mod.h"
@@ -124,8 +125,8 @@ const QString ExtractMod(const QString& path) {
fs::remove_all(tmp, ec); fs::remove_all(tmp, ec);
if (!fs::create_directories(tmp, ec)) { if (!fs::create_directories(tmp, ec)) {
QtCommon::Frontend::Critical(tr("Mod Extract Failed"), QtCommon::Frontend::Critical(tr("Mod Extract Failed"),
tr("Failed to create temporary directory %1") tr("Failed to create temporary directory %1")
.arg(QString::fromStdString(tmp.string()))); .arg(QString::fromStdString(tmp.string())));
return QString(); return QString();
} }