summaryrefslogtreecommitdiff
path: root/Source/UnitTests/Core/PatchAllowlistTest.cpp
diff options
context:
space:
mode:
authorAdmiral H. Curtiss <pikachu025@gmail.com>2024-07-07 23:48:09 +0200
committerGitHub <noreply@github.com>2024-07-07 23:48:09 +0200
commitb10d62cf66ac770d598b0d208338c43b73a67caa (patch)
treeff37b6cf24660e2cca4ebf2e70d2c0f9e7bbeac4 /Source/UnitTests/Core/PatchAllowlistTest.cpp
parent02e1b9414909db1644562b3669967feff203460f (diff)
parentae87bf9af5d6a09a380949b47f2d7c484cea7d01 (diff)
Merge pull request #12913 from LillyJadeKatrin/retroachievements-allowlist-test
RetroAchievements - Patch Allowlist Unit Test
Diffstat (limited to 'Source/UnitTests/Core/PatchAllowlistTest.cpp')
-rw-r--r--Source/UnitTests/Core/PatchAllowlistTest.cpp138
1 files changed, 138 insertions, 0 deletions
diff --git a/Source/UnitTests/Core/PatchAllowlistTest.cpp b/Source/UnitTests/Core/PatchAllowlistTest.cpp
new file mode 100644
index 0000000000..779b49791f
--- /dev/null
+++ b/Source/UnitTests/Core/PatchAllowlistTest.cpp
@@ -0,0 +1,138 @@
+// Copyright 2024 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <array>
+#include <map>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include <fmt/format.h>
+#include <gtest/gtest.h>
+#include <picojson.h>
+
+#include "Common/BitUtils.h"
+#include "Common/CommonPaths.h"
+#include "Common/Crypto/SHA1.h"
+#include "Common/FileUtil.h"
+#include "Common/IOFile.h"
+#include "Common/IniFile.h"
+#include "Common/JsonUtil.h"
+#include "Core/CheatCodes.h"
+#include "Core/PatchEngine.h"
+
+struct GameHashes
+{
+ std::string game_title;
+ std::map<std::string /*hash*/, std::string /*patch name*/> hashes;
+};
+
+TEST(PatchAllowlist, VerifyHashes)
+{
+ // Load allowlist
+ static constexpr std::string_view APPROVED_LIST_FILENAME = "ApprovedInis.json";
+ picojson::value json_tree;
+ std::string error;
+ std::string cur_directory = File::GetExeDirectory()
+#if defined(__APPLE__)
+ + DIR_SEP "Tests" // FIXME: Ugly hack.
+#endif
+ ;
+ std::string sys_directory = cur_directory + DIR_SEP "Sys";
+ const auto& list_filepath = fmt::format("{}{}{}", sys_directory, DIR_SEP, APPROVED_LIST_FILENAME);
+ ASSERT_TRUE(JsonFromFile(list_filepath, &json_tree, &error))
+ << "Failed to open file at " << list_filepath;
+ // Parse allowlist - Map<game id, Map<hash, name>
+ ASSERT_TRUE(json_tree.is<picojson::object>());
+ std::map<std::string /*ID*/, GameHashes> allow_list;
+ for (const auto& entry : json_tree.get<picojson::object>())
+ {
+ ASSERT_TRUE(entry.second.is<picojson::object>());
+ GameHashes& game_entry = allow_list[entry.first];
+ for (const auto& line : entry.second.get<picojson::object>())
+ {
+ ASSERT_TRUE(line.second.is<std::string>());
+ if (line.first == "title")
+ game_entry.game_title = line.second.get<std::string>();
+ else
+ game_entry.hashes[line.first] = line.second.get<std::string>();
+ }
+ }
+ // Iterate over GameSettings directory
+ auto directory =
+ File::ScanDirectoryTree(fmt::format("{}{}GameSettings", sys_directory, DIR_SEP), false);
+ for (const auto& file : directory.children)
+ {
+ // Load ini file
+ Common::IniFile ini_file;
+ ini_file.Load(file.physicalName, true);
+ std::string game_id = file.virtualName.substr(0, file.virtualName.find_first_of('.'));
+ std::vector<PatchEngine::Patch> patches;
+ PatchEngine::LoadPatchSection("OnFrame", &patches, ini_file, Common::IniFile());
+ // Filter patches for RetroAchievements approved
+ ReadEnabledOrDisabled<PatchEngine::Patch>(ini_file, "OnFrame", false, &patches);
+ ReadEnabledOrDisabled<PatchEngine::Patch>(ini_file, "Patches_RetroAchievements_Verified", true,
+ &patches);
+ // Get game section from allow list
+ auto game_itr = allow_list.find(game_id);
+ // Iterate over approved patches
+ for (const auto& patch : patches)
+ {
+ if (!patch.enabled)
+ continue;
+ // Hash patch
+ auto context = Common::SHA1::CreateContext();
+ context->Update(Common::BitCastToArray<u8>(static_cast<u64>(patch.entries.size())));
+ for (const auto& entry : patch.entries)
+ {
+ context->Update(Common::BitCastToArray<u8>(entry.type));
+ context->Update(Common::BitCastToArray<u8>(entry.address));
+ context->Update(Common::BitCastToArray<u8>(entry.value));
+ context->Update(Common::BitCastToArray<u8>(entry.comparand));
+ context->Update(Common::BitCastToArray<u8>(entry.conditional));
+ }
+ auto digest = context->Finish();
+ std::string hash = Common::SHA1::DigestToString(digest);
+ // Check patch in list
+ if (game_itr == allow_list.end())
+ {
+ // Report: no patches in game found in list
+ ADD_FAILURE() << "Approved hash missing from list." << std::endl
+ << "Game ID: " << game_id << std::endl
+ << "Patch: \"" << hash << "\" : \"" << patch.name << "\"";
+ continue;
+ }
+ auto hash_itr = game_itr->second.hashes.find(hash);
+ if (hash_itr == game_itr->second.hashes.end())
+ {
+ // Report: patch not found in list
+ ADD_FAILURE() << "Approved hash missing from list." << std::endl
+ << "Game ID: " << game_id << ":" << game_itr->second.game_title << std::endl
+ << "Patch: \"" << hash << "\" : \"" << patch.name << "\"";
+ }
+ else
+ {
+ // Remove patch from map if found
+ game_itr->second.hashes.erase(hash_itr);
+ }
+ }
+ // Report missing patches in map
+ if (game_itr == allow_list.end())
+ continue;
+ for (auto& remaining_hashes : game_itr->second.hashes)
+ {
+ ADD_FAILURE() << "Hash in list not approved in ini." << std::endl
+ << "Game ID: " << game_id << ":" << game_itr->second.game_title << std::endl
+ << "Patch: " << remaining_hashes.second << ":" << remaining_hashes.first;
+ }
+ // Remove section from map
+ allow_list.erase(game_itr);
+ }
+ // Report remaining sections in map
+ for (auto& remaining_games : allow_list)
+ {
+ ADD_FAILURE() << "Game in list has no ini file." << std::endl
+ << "Game ID: " << remaining_games.first << ":"
+ << remaining_games.second.game_title;
+ }
+}