PatchAllowlistTest.cpp 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. // Copyright 2024 Dolphin Emulator Project
  2. // SPDX-License-Identifier: GPL-2.0-or-later
  3. #include <array>
  4. #include <map>
  5. #include <string>
  6. #include <string_view>
  7. #include <vector>
  8. #include <fmt/format.h>
  9. #include <gtest/gtest.h>
  10. #include <picojson.h>
  11. #include "Common/BitUtils.h"
  12. #include "Common/CommonPaths.h"
  13. #include "Common/Crypto/SHA1.h"
  14. #include "Common/FileUtil.h"
  15. #include "Common/IOFile.h"
  16. #include "Common/IniFile.h"
  17. #include "Common/JsonUtil.h"
  18. #include "Core/CheatCodes.h"
  19. #include "Core/PatchEngine.h"
  20. struct GameHashes
  21. {
  22. std::string game_title;
  23. std::map<std::string /*hash*/, std::string /*patch name*/> hashes;
  24. };
  25. TEST(PatchAllowlist, VerifyHashes)
  26. {
  27. // Load allowlist
  28. static constexpr std::string_view APPROVED_LIST_FILENAME = "ApprovedInis.json";
  29. picojson::value json_tree;
  30. std::string error;
  31. std::string cur_directory = File::GetExeDirectory()
  32. #if defined(__APPLE__)
  33. + DIR_SEP "Tests" // FIXME: Ugly hack.
  34. #endif
  35. ;
  36. std::string sys_directory = cur_directory + DIR_SEP "Sys";
  37. const auto& list_filepath = fmt::format("{}{}{}", sys_directory, DIR_SEP, APPROVED_LIST_FILENAME);
  38. ASSERT_TRUE(JsonFromFile(list_filepath, &json_tree, &error))
  39. << "Failed to open file at " << list_filepath;
  40. // Parse allowlist - Map<game id, Map<hash, name>
  41. ASSERT_TRUE(json_tree.is<picojson::object>());
  42. std::map<std::string /*ID*/, GameHashes> allow_list;
  43. for (const auto& entry : json_tree.get<picojson::object>())
  44. {
  45. ASSERT_TRUE(entry.second.is<picojson::object>());
  46. GameHashes& game_entry = allow_list[entry.first];
  47. for (const auto& line : entry.second.get<picojson::object>())
  48. {
  49. ASSERT_TRUE(line.second.is<std::string>());
  50. if (line.first == "title")
  51. game_entry.game_title = line.second.get<std::string>();
  52. else
  53. game_entry.hashes[line.first] = line.second.get<std::string>();
  54. }
  55. }
  56. // Iterate over GameSettings directory
  57. auto directory =
  58. File::ScanDirectoryTree(fmt::format("{}{}GameSettings", sys_directory, DIR_SEP), false);
  59. for (const auto& file : directory.children)
  60. {
  61. // Load ini file
  62. Common::IniFile ini_file;
  63. ini_file.Load(file.physicalName, true);
  64. std::string game_id = file.virtualName.substr(0, file.virtualName.find_first_of('.'));
  65. std::vector<PatchEngine::Patch> patches;
  66. PatchEngine::LoadPatchSection("OnFrame", &patches, ini_file, Common::IniFile());
  67. // Filter patches for RetroAchievements approved
  68. ReadEnabledOrDisabled<PatchEngine::Patch>(ini_file, "OnFrame", false, &patches);
  69. ReadEnabledOrDisabled<PatchEngine::Patch>(ini_file, "Patches_RetroAchievements_Verified", true,
  70. &patches);
  71. // Get game section from allow list
  72. auto game_itr = allow_list.find(game_id);
  73. // Iterate over approved patches
  74. for (const auto& patch : patches)
  75. {
  76. if (!patch.enabled)
  77. continue;
  78. // Hash patch
  79. auto context = Common::SHA1::CreateContext();
  80. context->Update(Common::BitCastToArray<u8>(static_cast<u64>(patch.entries.size())));
  81. for (const auto& entry : patch.entries)
  82. {
  83. context->Update(Common::BitCastToArray<u8>(entry.type));
  84. context->Update(Common::BitCastToArray<u8>(entry.address));
  85. context->Update(Common::BitCastToArray<u8>(entry.value));
  86. context->Update(Common::BitCastToArray<u8>(entry.comparand));
  87. context->Update(Common::BitCastToArray<u8>(entry.conditional));
  88. }
  89. auto digest = context->Finish();
  90. std::string hash = Common::SHA1::DigestToString(digest);
  91. // Check patch in list
  92. if (game_itr == allow_list.end())
  93. {
  94. // Report: no patches in game found in list
  95. ADD_FAILURE() << "Approved hash missing from list." << std::endl
  96. << "Game ID: " << game_id << std::endl
  97. << "Patch: \"" << hash << "\" : \"" << patch.name << "\"";
  98. continue;
  99. }
  100. auto hash_itr = game_itr->second.hashes.find(hash);
  101. if (hash_itr == game_itr->second.hashes.end())
  102. {
  103. // Report: patch not found in list
  104. ADD_FAILURE() << "Approved hash missing from list." << std::endl
  105. << "Game ID: " << game_id << ":" << game_itr->second.game_title << std::endl
  106. << "Patch: \"" << hash << "\" : \"" << patch.name << "\"";
  107. }
  108. else
  109. {
  110. // Remove patch from map if found
  111. game_itr->second.hashes.erase(hash_itr);
  112. }
  113. }
  114. // Report missing patches in map
  115. if (game_itr == allow_list.end())
  116. continue;
  117. for (auto& remaining_hashes : game_itr->second.hashes)
  118. {
  119. ADD_FAILURE() << "Hash in list not approved in ini." << std::endl
  120. << "Game ID: " << game_id << ":" << game_itr->second.game_title << std::endl
  121. << "Patch: " << remaining_hashes.second << ":" << remaining_hashes.first;
  122. }
  123. // Remove section from map
  124. allow_list.erase(game_itr);
  125. }
  126. // Report remaining sections in map
  127. for (auto& remaining_games : allow_list)
  128. {
  129. ADD_FAILURE() << "Game in list has no ini file." << std::endl
  130. << "Game ID: " << remaining_games.first << ":"
  131. << remaining_games.second.game_title;
  132. }
  133. }