From ae87bf9af5d6a09a380949b47f2d7c484cea7d01 Mon Sep 17 00:00:00 2001 From: LillyJadeKatrin Date: Mon, 1 Jul 2024 20:49:51 -0400 Subject: [PATCH] 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. --- Source/UnitTests/CMakeLists.txt | 3 + Source/UnitTests/Core/CMakeLists.txt | 1 + Source/UnitTests/Core/PatchAllowlistTest.cpp | 138 +++++++++++++++++++ Source/UnitTests/UnitTests.vcxproj | 5 + 4 files changed, 147 insertions(+) create mode 100644 Source/UnitTests/Core/PatchAllowlistTest.cpp diff --git a/Source/UnitTests/CMakeLists.txt b/Source/UnitTests/CMakeLists.txt index 158b523cbe..96f26a8793 100644 --- a/Source/UnitTests/CMakeLists.txt +++ b/Source/UnitTests/CMakeLists.txt @@ -8,6 +8,9 @@ add_executable(tests EXCLUDE_FROM_ALL UnitTestsMain.cpp StubHost.cpp) set_target_properties(tests PROPERTIES FOLDER Tests) target_link_libraries(tests PRIVATE fmt::fmt gtest::gtest core uicommon) add_test(NAME tests COMMAND tests) +add_custom_command(TARGET tests POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_SOURCE_DIR}/Data/Sys" "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/Sys" +) add_dependencies(unittests tests) macro(add_dolphin_test target) 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 +#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; + } +} diff --git a/Source/UnitTests/UnitTests.vcxproj b/Source/UnitTests/UnitTests.vcxproj index 9e44cec8c3..229e5b576f 100644 --- a/Source/UnitTests/UnitTests.vcxproj +++ b/Source/UnitTests/UnitTests.vcxproj @@ -24,6 +24,9 @@ Console + + xcopy /i /e /s /y /f "$(ProjectDir)\..\..\Data\Sys\" "$(TargetDir)Sys" + @@ -70,6 +73,7 @@ + @@ -101,6 +105,7 @@ +