summaryrefslogtreecommitdiff
path: root/Source/UnitTests/Core/PatchAllowlistTest.cpp
blob: 779b49791f8b34a94791ea679aeb99511cbdec24 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
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;
  }
}