diff --git a/Source/UnitTests/Core/PatchAllowlistTest.cpp b/Source/UnitTests/Core/PatchAllowlistTest.cpp index 779b49791f..2c5bbf6361 100644 --- a/Source/UnitTests/Core/PatchAllowlistTest.cpp +++ b/Source/UnitTests/Core/PatchAllowlistTest.cpp @@ -18,7 +18,10 @@ #include "Common/IOFile.h" #include "Common/IniFile.h" #include "Common/JsonUtil.h" +#include "Core/ActionReplay.h" #include "Core/CheatCodes.h" +#include "Core/GeckoCode.h" +#include "Core/GeckoCodeConfig.h" #include "Core/PatchEngine.h" struct GameHashes @@ -27,6 +30,11 @@ struct GameHashes std::map hashes; }; +using AllowList = std::map; + +void CheckHash(const std::string& game_id, GameHashes* game_hashes, const std::string& hash, + const std::string& patch_name); + TEST(PatchAllowlist, VerifyHashes) { // Load allowlist @@ -42,9 +50,9 @@ TEST(PatchAllowlist, VerifyHashes) 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 + // Parse allowlist - Map> ASSERT_TRUE(json_tree.is()); - std::map allow_list; + AllowList allow_list; for (const auto& entry : json_tree.get()) { ASSERT_TRUE(entry.second.is()); @@ -69,12 +77,23 @@ TEST(PatchAllowlist, VerifyHashes) std::string game_id = file.virtualName.substr(0, file.virtualName.find_first_of('.')); std::vector patches; PatchEngine::LoadPatchSection("OnFrame", &patches, ini_file, Common::IniFile()); + std::vector geckos = Gecko::LoadCodes(Common::IniFile(), ini_file); + std::vector action_replays = + ActionReplay::LoadCodes(Common::IniFile(), ini_file); // Filter patches for RetroAchievements approved ReadEnabledOrDisabled(ini_file, "OnFrame", false, &patches); ReadEnabledOrDisabled(ini_file, "Patches_RetroAchievements_Verified", true, &patches); + ReadEnabledOrDisabled(ini_file, "Gecko", false, &geckos); + ReadEnabledOrDisabled(ini_file, "Gecko_Enabled", false, &geckos); + ReadEnabledOrDisabled(ini_file, "Gecko_RetroAchievements_Verified", true, + &geckos); + ReadEnabledOrDisabled(ini_file, "ActionReplay", false, &action_replays); + ReadEnabledOrDisabled(ini_file, "AR_RetroAchievements_Verified", true, + &action_replays); // Get game section from allow list auto game_itr = allow_list.find(game_id); + bool itr_end = (game_itr == allow_list.end()); // Iterate over approved patches for (const auto& patch : patches) { @@ -92,38 +111,51 @@ TEST(PatchAllowlist, VerifyHashes) context->Update(Common::BitCastToArray(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 << "\""; + CheckHash(game_id, itr_end ? nullptr : &game_itr->second, + Common::SHA1::DigestToString(digest), patch.name); + } + // Iterate over approved geckos + for (const auto& code : geckos) + { + if (!code.enabled) continue; - } - auto hash_itr = game_itr->second.hashes.find(hash); - if (hash_itr == game_itr->second.hashes.end()) + // Hash patch + auto context = Common::SHA1::CreateContext(); + context->Update(Common::BitCastToArray(static_cast(code.codes.size()))); + for (const auto& entry : code.codes) { - // 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 << "\""; + context->Update(Common::BitCastToArray(entry.address)); + context->Update(Common::BitCastToArray(entry.data)); } - else + auto digest = context->Finish(); + CheckHash(game_id, itr_end ? nullptr : &game_itr->second, + Common::SHA1::DigestToString(digest), code.name); + } + // Iterate over approved AR codes + for (const auto& code : action_replays) + { + if (!code.enabled) + continue; + // Hash patch + auto context = Common::SHA1::CreateContext(); + context->Update(Common::BitCastToArray(static_cast(code.ops.size()))); + for (const auto& entry : code.ops) { - // Remove patch from map if found - game_itr->second.hashes.erase(hash_itr); + context->Update(Common::BitCastToArray(entry.cmd_addr)); + context->Update(Common::BitCastToArray(entry.value)); } + auto digest = context->Finish(); + CheckHash(game_id, itr_end ? nullptr : &game_itr->second, + Common::SHA1::DigestToString(digest), code.name); } // Report missing patches in map - if (game_itr == allow_list.end()) + if (itr_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; + << "Code: " << remaining_hashes.second << ":" << remaining_hashes.first; } // Remove section from map allow_list.erase(game_itr); @@ -136,3 +168,30 @@ TEST(PatchAllowlist, VerifyHashes) << remaining_games.second.game_title; } } + +void CheckHash(const std::string& game_id, GameHashes* game_hashes, const std::string& hash, + const std::string& patch_name) +{ + // Check patch in list + if (game_hashes == nullptr) + { + // Report: no patches in game found in list + ADD_FAILURE() << "Approved hash missing from list." << std::endl + << "Game ID: " << game_id << std::endl + << "Code: \"" << hash << "\": \"" << patch_name << "\""; + return; + } + auto hash_itr = game_hashes->hashes.find(hash); + if (hash_itr == game_hashes->hashes.end()) + { + // Report: patch not found in list + ADD_FAILURE() << "Approved hash missing from list." << std::endl + << "Game ID: " << game_id << ":" << game_hashes->game_title << std::endl + << "Code: \"" << hash << "\": \"" << patch_name << "\""; + } + else + { + // Remove patch from map if found + game_hashes->hashes.erase(hash_itr); + } +}