summaryrefslogtreecommitdiff
path: root/Source/UnitTests/Core
diff options
context:
space:
mode:
authorLillyJadeKatrin <lilly.kitty.1988@gmail.com>2024-07-01 20:49:51 -0400
committerAdmiral H. Curtiss <pikachu025@gmail.com>2024-07-07 21:29:03 +0200
commitae87bf9af5d6a09a380949b47f2d7c484cea7d01 (patch)
treed570a952d22bccbe4a547db50d90e165b9669959 /Source/UnitTests/Core
parent3ca50f7879d79339fac66e79bdd2ddfa5e69235f (diff)
Add Unit Test for Patch Allowlist
This unit test compares ApprovedInis.json with the contents of the GameSettings folder to verify that every patch marked allowed for use with RetroAchievements has a hash in ApprovedInis.json. If not, that hash is reported in the test logs so that the hash may be updated more easily.
Diffstat (limited to 'Source/UnitTests/Core')
-rw-r--r--Source/UnitTests/Core/CMakeLists.txt1
-rw-r--r--Source/UnitTests/Core/PatchAllowlistTest.cpp138
2 files changed, 139 insertions, 0 deletions
diff --git a/Source/UnitTests/Core/CMakeLists.txt b/Source/UnitTests/Core/CMakeLists.txt
index f2de6f13ad..865064ed68 100644
--- a/Source/UnitTests/Core/CMakeLists.txt
+++ b/Source/UnitTests/Core/CMakeLists.txt
@@ -1,6 +1,7 @@
add_dolphin_test(MMIOTest MMIOTest.cpp)
add_dolphin_test(PageFaultTest PageFaultTest.cpp)
add_dolphin_test(CoreTimingTest CoreTimingTest.cpp)
+add_dolphin_test(PatchAllowlistTest PatchAllowlistTest.cpp)
add_dolphin_test(DSPAcceleratorTest DSP/DSPAcceleratorTest.cpp)
add_dolphin_test(DSPAssemblyTest
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;
+ }
+}