139 lines
5.2 KiB
C++
139 lines
5.2 KiB
C++
|
// 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;
|
||
|
}
|
||
|
}
|