// Copyright 2024 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include #include #include #include #include #include #include #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 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 ASSERT_TRUE(json_tree.is()); std::map allow_list; for (const auto& entry : json_tree.get()) { ASSERT_TRUE(entry.second.is()); GameHashes& game_entry = allow_list[entry.first]; for (const auto& line : entry.second.get()) { ASSERT_TRUE(line.second.is()); if (line.first == "title") game_entry.game_title = line.second.get(); else game_entry.hashes[line.first] = line.second.get(); } } // 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 patches; PatchEngine::LoadPatchSection("OnFrame", &patches, ini_file, Common::IniFile()); // Filter patches for RetroAchievements approved ReadEnabledOrDisabled(ini_file, "OnFrame", false, &patches); ReadEnabledOrDisabled(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(static_cast(patch.entries.size()))); for (const auto& entry : patch.entries) { context->Update(Common::BitCastToArray(entry.type)); context->Update(Common::BitCastToArray(entry.address)); context->Update(Common::BitCastToArray(entry.value)); context->Update(Common::BitCastToArray(entry.comparand)); 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 << "\""; 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; } }