diff options
| author | Admiral H. Curtiss <pikachu025@gmail.com> | 2024-07-07 23:48:09 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-07-07 23:48:09 +0200 |
| commit | b10d62cf66ac770d598b0d208338c43b73a67caa (patch) | |
| tree | ff37b6cf24660e2cca4ebf2e70d2c0f9e7bbeac4 /Source | |
| parent | 02e1b9414909db1644562b3669967feff203460f (diff) | |
| parent | ae87bf9af5d6a09a380949b47f2d7c484cea7d01 (diff) | |
Merge pull request #12913 from LillyJadeKatrin/retroachievements-allowlist-test
RetroAchievements - Patch Allowlist Unit Test
Diffstat (limited to 'Source')
| -rw-r--r-- | Source/Core/Common/FileUtil.cpp | 13 | ||||
| -rw-r--r-- | Source/Core/Core/AchievementManager.cpp | 72 | ||||
| -rw-r--r-- | Source/Core/Core/AchievementManager.h | 20 | ||||
| -rw-r--r-- | Source/Core/Core/PatchEngine.cpp | 10 | ||||
| -rw-r--r-- | Source/UnitTests/CMakeLists.txt | 3 | ||||
| -rw-r--r-- | Source/UnitTests/Core/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | Source/UnitTests/Core/PatchAllowlistTest.cpp | 138 | ||||
| -rw-r--r-- | Source/UnitTests/UnitTests.vcxproj | 5 |
8 files changed, 258 insertions, 4 deletions
diff --git a/Source/Core/Common/FileUtil.cpp b/Source/Core/Common/FileUtil.cpp index 5582018b30..545bfaba44 100644 --- a/Source/Core/Common/FileUtil.cpp +++ b/Source/Core/Common/FileUtil.cpp @@ -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)); diff --git a/Source/Core/Core/AchievementManager.cpp b/Source/Core/Core/AchievementManager.cpp index 544ec36afc..788a394be4 100644 --- a/Source/Core/Core/AchievementManager.cpp +++ b/Source/Core/Core/AchievementManager.cpp @@ -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)); diff --git a/Source/Core/Core/AchievementManager.h b/Source/Core/Core/AchievementManager.h index 3996003040..efddd003a9 100644 --- a/Source/Core/Core/AchievementManager.h +++ b/Source/Core/Core/AchievementManager.h @@ -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; diff --git a/Source/Core/Core/PatchEngine.cpp b/Source/Core/Core/PatchEngine.cpp index f020c754eb..0728637e47 100644 --- a/Source/Core/Core/PatchEngine.cpp +++ b/Source/Core/Core/PatchEngine.cpp @@ -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) 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 <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; + } +} 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 @@ <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> |
