mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-07-23 14:19:46 -06:00
Merge pull request #12913 from LillyJadeKatrin/retroachievements-allowlist-test
RetroAchievements - Patch Allowlist Unit Test
This commit is contained in:
@ -60,6 +60,10 @@
|
||||
#include "jni/AndroidCommon/AndroidCommon.h"
|
||||
#endif
|
||||
|
||||
#if defined(__FreeBSD__)
|
||||
#include <sys/sysctl.h>
|
||||
#endif
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace File
|
||||
@ -738,6 +742,15 @@ std::string GetExePath()
|
||||
return PathToString(exe_path_absolute);
|
||||
#elif defined(__APPLE__)
|
||||
return GetBundleDirectory();
|
||||
#elif defined(__FreeBSD__)
|
||||
int name[4]{CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1};
|
||||
size_t length = 0;
|
||||
if (sysctl(name, 4, nullptr, &length, nullptr, 0) != 0 || length == 0)
|
||||
return {};
|
||||
std::string dolphin_exe_path(length, '\0');
|
||||
if (sysctl(name, 4, dolphin_exe_path.data(), &length, nullptr, 0) != 0)
|
||||
return {};
|
||||
return dolphin_exe_path;
|
||||
#else
|
||||
char dolphin_exe_path[PATH_MAX];
|
||||
ssize_t len = ::readlink("/proc/self/exe", dolphin_exe_path, sizeof(dolphin_exe_path));
|
||||
|
@ -14,6 +14,7 @@
|
||||
#include <rcheevos/include/rc_hash.h>
|
||||
|
||||
#include "Common/Assert.h"
|
||||
#include "Common/BitUtils.h"
|
||||
#include "Common/CommonPaths.h"
|
||||
#include "Common/FileUtil.h"
|
||||
#include "Common/IOFile.h"
|
||||
@ -26,6 +27,7 @@
|
||||
#include "Core/Core.h"
|
||||
#include "Core/HW/Memmap.h"
|
||||
#include "Core/HW/VideoInterface.h"
|
||||
#include "Core/PatchEngine.h"
|
||||
#include "Core/PowerPC/MMU.h"
|
||||
#include "Core/System.h"
|
||||
#include "DiscIO/Blob.h"
|
||||
@ -70,6 +72,34 @@ void AchievementManager::Init()
|
||||
}
|
||||
}
|
||||
|
||||
void AchievementManager::LoadApprovedList()
|
||||
{
|
||||
picojson::value temp;
|
||||
std::string error;
|
||||
if (!JsonFromFile(fmt::format("{}{}{}", File::GetSysDirectory(), DIR_SEP, APPROVED_LIST_FILENAME),
|
||||
&temp, &error))
|
||||
{
|
||||
WARN_LOG_FMT(ACHIEVEMENTS, "Failed to load approved game settings list {}",
|
||||
APPROVED_LIST_FILENAME);
|
||||
WARN_LOG_FMT(ACHIEVEMENTS, "Error: {}", error);
|
||||
return;
|
||||
}
|
||||
auto context = Common::SHA1::CreateContext();
|
||||
context->Update(temp.serialize());
|
||||
auto digest = context->Finish();
|
||||
if (digest != APPROVED_LIST_HASH)
|
||||
{
|
||||
WARN_LOG_FMT(ACHIEVEMENTS, "Failed to verify approved game settings list {}",
|
||||
APPROVED_LIST_FILENAME);
|
||||
WARN_LOG_FMT(ACHIEVEMENTS, "Expected hash {}, found hash {}",
|
||||
Common::SHA1::DigestToString(APPROVED_LIST_HASH),
|
||||
Common::SHA1::DigestToString(digest));
|
||||
return;
|
||||
}
|
||||
std::lock_guard lg{m_lock};
|
||||
m_ini_root = std::move(temp);
|
||||
}
|
||||
|
||||
void AchievementManager::SetUpdateCallback(UpdateCallback callback)
|
||||
{
|
||||
m_update_callback = std::move(callback);
|
||||
@ -322,6 +352,48 @@ bool AchievementManager::IsHardcoreModeActive() const
|
||||
return rc_client_is_processing_required(m_client);
|
||||
}
|
||||
|
||||
void AchievementManager::FilterApprovedPatches(std::vector<PatchEngine::Patch>& patches,
|
||||
const std::string& game_ini_id) const
|
||||
{
|
||||
if (!IsHardcoreModeActive())
|
||||
return;
|
||||
|
||||
if (!m_ini_root.contains(game_ini_id))
|
||||
patches.clear();
|
||||
auto patch_itr = patches.begin();
|
||||
while (patch_itr != patches.end())
|
||||
{
|
||||
INFO_LOG_FMT(ACHIEVEMENTS, "Verifying patch {}", patch_itr->name);
|
||||
|
||||
auto context = Common::SHA1::CreateContext();
|
||||
context->Update(Common::BitCastToArray<u8>(static_cast<u64>(patch_itr->entries.size())));
|
||||
for (const auto& entry : patch_itr->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();
|
||||
|
||||
bool verified = m_ini_root.get(game_ini_id).contains(Common::SHA1::DigestToString(digest));
|
||||
if (!verified)
|
||||
{
|
||||
patch_itr = patches.erase(patch_itr);
|
||||
OSD::AddMessage(
|
||||
fmt::format("Failed to verify patch {} from file {}.", patch_itr->name, game_ini_id),
|
||||
OSD::Duration::VERY_LONG, OSD::Color::RED);
|
||||
OSD::AddMessage("Disable hardcore mode to enable this patch.", OSD::Duration::VERY_LONG,
|
||||
OSD::Color::RED);
|
||||
}
|
||||
else
|
||||
{
|
||||
patch_itr++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AchievementManager::SetSpectatorMode()
|
||||
{
|
||||
rc_client_set_spectator_mode_enabled(m_client, Config::Get(Config::RA_SPECTATOR_ENABLED));
|
||||
|
@ -27,6 +27,7 @@
|
||||
#include "Common/CommonTypes.h"
|
||||
#include "Common/Event.h"
|
||||
#include "Common/HttpRequest.h"
|
||||
#include "Common/JsonUtil.h"
|
||||
#include "Common/WorkQueueThread.h"
|
||||
#include "DiscIO/Volume.h"
|
||||
#include "VideoCommon/Assets/CustomTextureData.h"
|
||||
@ -37,6 +38,11 @@ class CPUThreadGuard;
|
||||
class System;
|
||||
} // namespace Core
|
||||
|
||||
namespace PatchEngine
|
||||
{
|
||||
struct Patch;
|
||||
} // namespace PatchEngine
|
||||
|
||||
class AchievementManager
|
||||
{
|
||||
public:
|
||||
@ -60,6 +66,10 @@ public:
|
||||
static constexpr std::string_view GRAY = "transparent";
|
||||
static constexpr std::string_view GOLD = "#FFD700";
|
||||
static constexpr std::string_view BLUE = "#0B71C1";
|
||||
static constexpr std::string_view APPROVED_LIST_FILENAME = "ApprovedInis.json";
|
||||
static const inline Common::SHA1::Digest APPROVED_LIST_HASH = {
|
||||
0x01, 0x1E, 0x2E, 0x74, 0xDD, 0x07, 0x79, 0xDA, 0x0E, 0x5D,
|
||||
0xF8, 0x51, 0x09, 0xC7, 0x9B, 0x46, 0x22, 0x95, 0x50, 0xE9};
|
||||
|
||||
struct LeaderboardEntry
|
||||
{
|
||||
@ -109,6 +119,9 @@ public:
|
||||
std::recursive_mutex& GetLock();
|
||||
void SetHardcoreMode();
|
||||
bool IsHardcoreModeActive() const;
|
||||
void SetGameIniId(const std::string& game_ini_id) { m_game_ini_id = game_ini_id; }
|
||||
void FilterApprovedPatches(std::vector<PatchEngine::Patch>& patches,
|
||||
const std::string& game_ini_id) const;
|
||||
void SetSpectatorMode();
|
||||
std::string_view GetPlayerDisplayName() const;
|
||||
u32 GetPlayerScore() const;
|
||||
@ -132,7 +145,7 @@ public:
|
||||
void Shutdown();
|
||||
|
||||
private:
|
||||
AchievementManager() = default;
|
||||
AchievementManager() { LoadApprovedList(); };
|
||||
|
||||
struct FilereaderState
|
||||
{
|
||||
@ -140,6 +153,8 @@ private:
|
||||
std::unique_ptr<DiscIO::Volume> volume;
|
||||
};
|
||||
|
||||
void LoadApprovedList();
|
||||
|
||||
static void* FilereaderOpenByFilepath(const char* path_utf8);
|
||||
static void* FilereaderOpenByVolume(const char* path_utf8);
|
||||
static void FilereaderSeek(void* file_handle, int64_t offset, int origin);
|
||||
@ -211,6 +226,9 @@ private:
|
||||
std::chrono::steady_clock::time_point m_last_rp_time = std::chrono::steady_clock::now();
|
||||
std::chrono::steady_clock::time_point m_last_progress_message = std::chrono::steady_clock::now();
|
||||
|
||||
picojson::value m_ini_root;
|
||||
std::string m_game_ini_id;
|
||||
|
||||
std::unordered_map<AchievementId, LeaderboardStatus> m_leaderboard_map;
|
||||
bool m_challenges_updated = false;
|
||||
std::unordered_set<AchievementId> m_active_challenges;
|
||||
|
@ -182,6 +182,13 @@ void LoadPatches()
|
||||
|
||||
LoadPatchSection("OnFrame", &s_on_frame, globalIni, localIni);
|
||||
|
||||
#ifdef USE_RETRO_ACHIEVEMENTS
|
||||
{
|
||||
std::lock_guard lg{AchievementManager::GetInstance().GetLock()};
|
||||
AchievementManager::GetInstance().FilterApprovedPatches(s_on_frame, sconfig.GetGameID());
|
||||
}
|
||||
#endif // USE_RETRO_ACHIEVEMENTS
|
||||
|
||||
// Check if I'm syncing Codes
|
||||
if (Config::Get(Config::SESSION_CODE_SYNC_OVERRIDE))
|
||||
{
|
||||
@ -197,9 +204,6 @@ void LoadPatches()
|
||||
|
||||
static void ApplyPatches(const Core::CPUThreadGuard& guard, const std::vector<Patch>& patches)
|
||||
{
|
||||
if (AchievementManager::GetInstance().IsHardcoreModeActive())
|
||||
return;
|
||||
|
||||
for (const Patch& patch : patches)
|
||||
{
|
||||
if (patch.enabled)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
138
Source/UnitTests/Core/PatchAllowlistTest.cpp
Normal file
138
Source/UnitTests/Core/PatchAllowlistTest.cpp
Normal file
@ -0,0 +1,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;
|
||||
}
|
||||
}
|
@ -24,6 +24,9 @@
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
</Link>
|
||||
<PostBuildEvent>
|
||||
<Command>xcopy /i /e /s /y /f "$(ProjectDir)\..\..\Data\Sys\" "$(TargetDir)Sys"</Command>
|
||||
</PostBuildEvent>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="Core\DSP\DSPTestBinary.h" />
|
||||
@ -70,6 +73,7 @@
|
||||
<ClCompile Include="Core\IOS\USB\SkylandersTest.cpp" />
|
||||
<ClCompile Include="Core\MMIOTest.cpp" />
|
||||
<ClCompile Include="Core\PageFaultTest.cpp" />
|
||||
<ClCompile Include="Core\PatchAllowlistTest.cpp" />
|
||||
<ClCompile Include="Core\PowerPC\DivUtilsTest.cpp" />
|
||||
<ClCompile Include="VideoCommon\VertexLoaderTest.cpp" />
|
||||
<ClCompile Include="StubHost.cpp" />
|
||||
@ -101,6 +105,7 @@
|
||||
</ItemGroup>
|
||||
<Import Project="$(ExternalsDir)Bochs_disasm\exports.props" />
|
||||
<Import Project="$(ExternalsDir)fmt\exports.props" />
|
||||
<Import Project="$(ExternalsDir)picojson\exports.props" />
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
</ImportGroup>
|
||||
|
Reference in New Issue
Block a user