From b5841d36b6d49afba7bc82a636e8c15d8fc7cc17 Mon Sep 17 00:00:00 2001 From: digant73 Date: Fri, 1 May 2026 23:26:57 +0200 Subject: [PATCH 1/9] Add integrity check to PSN Content, DLC and Game Update files --- rpcs3/Emu/CMakeLists.txt | 2 +- rpcs3/Emu/system_utils.cpp | 30 +++ rpcs3/Emu/system_utils.hpp | 6 + rpcs3/Loader/content_validation.cpp | 180 ++++++++++++++++++ rpcs3/Loader/content_validation.h | 51 +++++ rpcs3/Loader/iso_validation.cpp | 162 ---------------- rpcs3/Loader/iso_validation.h | 42 ---- rpcs3/emucore.vcxproj | 5 +- rpcs3/emucore.vcxproj.filters | 10 +- rpcs3/rpcs3.vcxproj | 12 +- rpcs3/rpcs3.vcxproj.filters | 8 +- rpcs3/rpcs3qt/CMakeLists.txt | 2 +- rpcs3/rpcs3qt/content_integrity.cpp | 136 +++++++++++++ .../{iso_integrity.h => content_integrity.h} | 13 +- rpcs3/rpcs3qt/game_list_actions.cpp | 51 +++-- rpcs3/rpcs3qt/game_list_actions.h | 6 +- rpcs3/rpcs3qt/game_list_context_menu.cpp | 81 ++++++-- rpcs3/rpcs3qt/game_list_frame.cpp | 5 +- rpcs3/rpcs3qt/game_list_frame.h | 12 +- rpcs3/rpcs3qt/iso_integrity.cpp | 113 ----------- 20 files changed, 557 insertions(+), 370 deletions(-) create mode 100644 rpcs3/Loader/content_validation.cpp create mode 100644 rpcs3/Loader/content_validation.h delete mode 100644 rpcs3/Loader/iso_validation.cpp delete mode 100644 rpcs3/Loader/iso_validation.h create mode 100644 rpcs3/rpcs3qt/content_integrity.cpp rename rpcs3/rpcs3qt/{iso_integrity.h => content_integrity.h} (57%) delete mode 100644 rpcs3/rpcs3qt/iso_integrity.cpp diff --git a/rpcs3/Emu/CMakeLists.txt b/rpcs3/Emu/CMakeLists.txt index f863a2b911..2c0fdadb58 100644 --- a/rpcs3/Emu/CMakeLists.txt +++ b/rpcs3/Emu/CMakeLists.txt @@ -127,7 +127,7 @@ target_sources(rpcs3_emu PRIVATE ../Loader/TAR.cpp ../Loader/ISO.cpp ../Loader/iso_cache.cpp - ../Loader/iso_validation.cpp + ../Loader/content_validation.cpp ../Loader/TROPUSR.cpp ../Loader/TRP.cpp ) diff --git a/rpcs3/Emu/system_utils.cpp b/rpcs3/Emu/system_utils.cpp index b1235dabf6..0b8e8d0886 100644 --- a/rpcs3/Emu/system_utils.cpp +++ b/rpcs3/Emu/system_utils.cpp @@ -256,6 +256,36 @@ namespace rpcs3::utils return get_data_dir() + "redump/"; } + std::string get_psn_content_db_path() + { + return fs::get_config_dir(true) + "psn_content.dat"; + } + + std::string get_psn_content_db_download_url() + { + return "https://api.rpcs3.net/psn_content/?api=v1"; + } + + std::string get_psn_dlc_db_path() + { + return fs::get_config_dir(true) + "psn_dlc.dat"; + } + + std::string get_psn_dlc_db_download_url() + { + return "https://api.rpcs3.net/psn_dlc/?api=v1"; + } + + std::string get_psn_update_db_path() + { + return fs::get_config_dir(true) + "psn_update.dat"; + } + + std::string get_psn_update_db_download_url() + { + return "https://api.rpcs3.net/psn_update/?api=v1"; + } + std::string get_data_dir() { return fs::get_config_dir() + "data/"; diff --git a/rpcs3/Emu/system_utils.hpp b/rpcs3/Emu/system_utils.hpp index 1545cbf829..8745b7d3b0 100644 --- a/rpcs3/Emu/system_utils.hpp +++ b/rpcs3/Emu/system_utils.hpp @@ -49,6 +49,12 @@ namespace rpcs3::utils std::string get_redump_db_path(); std::string get_redump_db_download_url(); std::string get_redump_key_dir(); + std::string get_psn_content_db_path(); + std::string get_psn_content_db_download_url(); + std::string get_psn_dlc_db_path(); + std::string get_psn_dlc_db_download_url(); + std::string get_psn_update_db_path(); + std::string get_psn_update_db_download_url(); std::string get_data_dir(); std::string get_icons_dir(); diff --git a/rpcs3/Loader/content_validation.cpp b/rpcs3/Loader/content_validation.cpp new file mode 100644 index 0000000000..5bf9bbe072 --- /dev/null +++ b/rpcs3/Loader/content_validation.cpp @@ -0,0 +1,180 @@ +#include "stdafx.h" + +#include "content_validation.h" + +#include "Emu/system_utils.hpp" +#include "Utilities/File.h" +#include "Utilities/rXml.h" +#include "Crypto/md5.h" +#include "Crypto/utils.h" + +LOG_CHANNEL(sys_log, "VALIDATION"); + +content_integrity_status content_validation::check_integrity(content_file_type file_type, const std::string& hash, std::string* game_name) +{ + // + // Check for Redump db + // + + std::string db_path; + + switch (file_type) + { + case content_file_type::ISO: + db_path = rpcs3::utils::get_redump_db_path(); + break; + case content_file_type::PSN_CONTENT: + db_path = rpcs3::utils::get_psn_content_db_path(); + break; + case content_file_type::PSN_DLC: + db_path = rpcs3::utils::get_psn_dlc_db_path(); + break; + case content_file_type::PSN_UPDATE: + db_path = rpcs3::utils::get_psn_update_db_path(); + break; + default: // Let the following opening attempt fail and log the error message + break; + } + + fs::file db_file(db_path); + + // If no db file exists + if (!db_file) + { + // An empty hash is used to simply test the presence (without any logging) of the Redump db + if (!hash.empty()) + { + sys_log.error("check_integrity: Failed to open file: %s", db_path); + } + + return content_integrity_status::ERROR_OPENING_DB; + } + + if (hash.empty()) + { + return content_integrity_status::NO_MATCH; + } + + rXmlDocument db; + + if (!db.Read(db_file.to_string())) + { + sys_log.error("check_integrity: Failed to process file: %s", db_path); + return content_integrity_status::ERROR_PARSING_DB; + } + + std::shared_ptr db_base = db.GetRoot(); + + if (!db_base) + { + sys_log.error("check_integrity: Failed to get 'root' node on file: %s", db_path); + return content_integrity_status::ERROR_PARSING_DB; + } + + if (db_base = db_base->GetChild(std::string_view("datafile")); !db_base) + { + sys_log.error("check_integrity: Failed to get 'datafile' node on file: %s", db_path); + return content_integrity_status::ERROR_PARSING_DB; + } + + // + // Check for a match on Redump db + // + + for (auto node = db_base->GetChildren(); node; node = node->GetNext()) + { + if (node->GetName() == "game") + { + for (auto child = node->GetChildren(); child; child = child->GetNext()) + { + // If a match is found, fill in "game_desc" (if requested) and return FOUND_MATCH + if (child->GetName() == "rom" && hash == child->GetAttribute(std::string_view("md5"))) + { + if (game_name) + { + *game_name = node->GetAttribute(std::string_view("name")); + } + + return content_integrity_status::FOUND_MATCH; + } + } + } + } + + // No match found + return content_integrity_status::NO_MATCH; +} + +bool content_validation::init_hash(const std::string& path) +{ + fs::file iso_file(path); + + // If no ISO file exists + if (!iso_file) + { + sys_log.error("init_hash: Failed to open file: %s", path); + m_status = content_hash_status::ABORTED; + return false; + } + + m_path = path; + m_size = iso_file.size(); + m_bytes_read = 0; + m_status = content_hash_status::INITIALIZED; + return true; +} + +content_hash_status content_validation::calculate_hash(std::string& hash) +{ + if (m_status != content_hash_status::INITIALIZED) + { + sys_log.error("calculate_hash: MD5 hash calculation already performed: %s", m_path); + m_status = content_hash_status::ABORTED; + return m_status; + } + + fs::file iso_file(m_path); + + // If no ISO file exists + if (!iso_file) + { + sys_log.error("calculate_hash: Failed to open file: %s", m_path); + m_status = content_hash_status::ABORTED; + return m_status; + } + + constexpr u64 block_size = 4096; + std::array buf; + u64 bytes_read; + mbedtls_md5_context md5_ctx; + unsigned char md5_hash[16]; + + mbedtls_md5_starts_ret(&md5_ctx); + + do + { + bytes_read = iso_file.read(buf.data(), block_size); + mbedtls_md5_update_ret(&md5_ctx, buf.data(), bytes_read); + + m_bytes_read += bytes_read; + } while (bytes_read == block_size && m_status != content_hash_status::ABORTED); + + if (m_status == content_hash_status::ABORTED) + { + sys_log.warning("calculate_hash: MD5 hash calculation aborted by user: %s", m_path); + return m_status; + } + + if (mbedtls_md5_finish_ret(&md5_ctx, md5_hash) != 0) + { + sys_log.error("calculate_hash: Failed to calculate MD5 hash on file: %s", m_path); + m_status = content_hash_status::ABORTED; + return m_status; + } + + // Convert the MD5 hash to hex string + bytes_to_hex(hash, md5_hash, 16); + + m_status = content_hash_status::COMPLETED; + return m_status; +} diff --git a/rpcs3/Loader/content_validation.h b/rpcs3/Loader/content_validation.h new file mode 100644 index 0000000000..1c20d02148 --- /dev/null +++ b/rpcs3/Loader/content_validation.h @@ -0,0 +1,51 @@ +#pragma once + +#include "util/types.hpp" + +// Enum identifying the content file type +enum class content_file_type +{ + ISO, + PSN_CONTENT, + PSN_DLC, + PSN_UPDATE +}; + +// Enum returned by calculating hash +enum class content_hash_status +{ + INITIALIZED, + COMPLETED, + ABORTED +}; + +// Enum returned by checking integrity +enum class content_integrity_status +{ + NO_MATCH, + FOUND_MATCH, + ERROR_OPENING_DB, + ERROR_PARSING_DB +}; + +// Content validation class +class content_validation +{ +private: + std::string m_path; + u64 m_size = 0; + u64 m_bytes_read = 0; + content_hash_status m_status = content_hash_status::INITIALIZED; + +public: + static content_integrity_status check_integrity(content_file_type file_type, const std::string& hash, std::string* game_name = nullptr); + + const std::string& get_path() const { return m_path; } + u64 get_size() const { return m_size; } + u64 get_bytes_read() const { return m_bytes_read; } + content_hash_status get_status() const { return m_status; } + void abort_hash() { m_status = content_hash_status::ABORTED; } + + bool init_hash(const std::string& path); + content_hash_status calculate_hash(std::string& hash); +}; diff --git a/rpcs3/Loader/iso_validation.cpp b/rpcs3/Loader/iso_validation.cpp deleted file mode 100644 index 31a82beaa7..0000000000 --- a/rpcs3/Loader/iso_validation.cpp +++ /dev/null @@ -1,162 +0,0 @@ -#include "stdafx.h" - -#include "iso_validation.h" -#include "ISO.h" - -#include "Emu/system_utils.hpp" -#include "Utilities/File.h" -#include "Utilities/rXml.h" -#include "Crypto/md5.h" -#include "Crypto/utils.h" - -LOG_CHANNEL(iso_log, "ISO"); - -iso_integrity_status iso_file_validation::check_integrity(const std::string& hash, std::string* game_name) -{ - // - // Check for Redump db - // - - const std::string db_path = rpcs3::utils::get_redump_db_path(); - fs::file db_file(db_path); - - // If no db file exists - if (!db_file) - { - // An empty hash is used to simply test the presence (without any logging) of the Redump db - if (!hash.empty()) - { - iso_log.error("check_integrity: Failed to open file: %s", db_path); - } - - return iso_integrity_status::ERROR_OPENING_DB; - } - - if (hash.empty()) - { - return iso_integrity_status::NO_MATCH; - } - - rXmlDocument db; - - if (!db.Read(db_file.to_string())) - { - iso_log.error("check_integrity: Failed to process file: %s", db_path); - return iso_integrity_status::ERROR_PARSING_DB; - } - - std::shared_ptr db_base = db.GetRoot(); - - if (!db_base) - { - iso_log.error("check_integrity: Failed to get 'root' node on file: %s", db_path); - return iso_integrity_status::ERROR_PARSING_DB; - } - - if (db_base = db_base->GetChild(std::string_view("datafile")); !db_base) - { - iso_log.error("check_integrity: Failed to get 'datafile' node on file: %s", db_path); - return iso_integrity_status::ERROR_PARSING_DB; - } - - // - // Check for a match on Redump db - // - - for (auto node = db_base->GetChildren(); node; node = node->GetNext()) - { - if (node->GetName() == "game") - { - if (const auto child = node->GetChild(std::string_view("rom"))) - { - // If a match is found, fill in "game_desc" (if requested) and return FOUND_MATCH - if (hash == child->GetAttribute(std::string_view("md5"))) - { - if (game_name) - { - *game_name = node->GetAttribute(std::string_view("name")); - } - - return iso_integrity_status::FOUND_MATCH; - } - } - } - } - - // No match found - return iso_integrity_status::NO_MATCH; -} - -bool iso_file_validation::init_hash(const std::string& path) -{ - fs::file iso_file(path); - - // If no ISO file exists - if (!iso_file) - { - iso_log.error("init_hash: Failed to open file: %s", path); - m_status = iso_hash_status::ABORTED; - return false; - } - - m_path = path; - m_size = iso_file.size(); - m_bytes_read = 0; - m_status = iso_hash_status::INITIALIZED; - return true; -} - -iso_hash_status iso_file_validation::calculate_hash(std::string& hash) -{ - if (m_status != iso_hash_status::INITIALIZED) - { - iso_log.error("calculate_hash: MD5 hash calculation already performed: %s", m_path); - m_status = iso_hash_status::ABORTED; - return m_status; - } - - fs::file iso_file(m_path); - - // If no ISO file exists - if (!iso_file) - { - iso_log.error("calculate_hash: Failed to open file: %s", m_path); - m_status = iso_hash_status::ABORTED; - return m_status; - } - - constexpr u64 block_size = ISO_SECTOR_SIZE * 2; - std::array buf; - u64 bytes_read; - mbedtls_md5_context md5_ctx; - unsigned char md5_hash[16]; - - mbedtls_md5_starts_ret(&md5_ctx); - - do - { - bytes_read = iso_file.read(buf.data(), block_size); - mbedtls_md5_update_ret(&md5_ctx, buf.data(), bytes_read); - - m_bytes_read += bytes_read; - } while (bytes_read == block_size && m_status != iso_hash_status::ABORTED); - - if (m_status == iso_hash_status::ABORTED) - { - iso_log.warning("calculate_hash: MD5 hash calculation aborted by user: %s", m_path); - return m_status; - } - - if (mbedtls_md5_finish_ret(&md5_ctx, md5_hash) != 0) - { - iso_log.error("calculate_hash: Failed to calculate MD5 hash on file: %s", m_path); - m_status = iso_hash_status::ABORTED; - return m_status; - } - - // Convert the MD5 hash to hex string - bytes_to_hex(hash, md5_hash, 16); - - m_status = iso_hash_status::COMPLETED; - return m_status; -} diff --git a/rpcs3/Loader/iso_validation.h b/rpcs3/Loader/iso_validation.h deleted file mode 100644 index 56ac659ce8..0000000000 --- a/rpcs3/Loader/iso_validation.h +++ /dev/null @@ -1,42 +0,0 @@ -#pragma once - -#include "util/types.hpp" - -// Enum returned by calculating hash -enum class iso_hash_status -{ - INITIALIZED, - COMPLETED, - ABORTED -}; - -// Enum returned by checking integrity -enum class iso_integrity_status -{ - NO_MATCH, - FOUND_MATCH, - ERROR_OPENING_DB, - ERROR_PARSING_DB -}; - -// ISO file validation class -class iso_file_validation -{ -private: - std::string m_path; - u64 m_size = 0; - u64 m_bytes_read = 0; - iso_hash_status m_status = iso_hash_status::INITIALIZED; - -public: - static iso_integrity_status check_integrity(const std::string& hash, std::string* game_name = nullptr); - - const std::string& get_path() const { return m_path; } - u64 get_size() const { return m_size; } - u64 get_bytes_read() const { return m_bytes_read; } - iso_hash_status get_status() const { return m_status; } - void abort_hash() { m_status = iso_hash_status::ABORTED; } - - bool init_hash(const std::string& path); - iso_hash_status calculate_hash(std::string& hash); -}; diff --git a/rpcs3/emucore.vcxproj b/rpcs3/emucore.vcxproj index 3606a20918..d367715689 100644 --- a/rpcs3/emucore.vcxproj +++ b/rpcs3/emucore.vcxproj @@ -203,7 +203,7 @@ - + NotUsing @@ -777,7 +777,7 @@ - + @@ -1061,6 +1061,7 @@ + diff --git a/rpcs3/emucore.vcxproj.filters b/rpcs3/emucore.vcxproj.filters index 87bf4fed67..59e23dc147 100644 --- a/rpcs3/emucore.vcxproj.filters +++ b/rpcs3/emucore.vcxproj.filters @@ -1411,6 +1411,9 @@ Loader + + Loader + Emu\GPU\RSX\Overlays @@ -1429,7 +1432,7 @@ Emu\GPU\RSX\Overlays - + Loader @@ -2854,6 +2857,9 @@ Loader + + Loader + Emu\GPU\RSX\Overlays @@ -2878,7 +2884,7 @@ Emu\GPU\RSX\Common - + Loader diff --git a/rpcs3/rpcs3.vcxproj b/rpcs3/rpcs3.vcxproj index 8bf5e5bc4f..476e78d9eb 100644 --- a/rpcs3/rpcs3.vcxproj +++ b/rpcs3/rpcs3.vcxproj @@ -341,7 +341,7 @@ true - + true @@ -653,7 +653,7 @@ true - + true @@ -928,7 +928,7 @@ - + @@ -1922,13 +1922,13 @@ .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DNDEBUG -DQT_CONCURRENT_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtConcurrent" - + $(QTDIR)\bin\moc.exe;%(FullPath) - Moc%27ing iso_integrity.h... + Moc%27ing content_integrity.h... .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DQT_CONCURRENT_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtCore" "-I.\debug" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtConcurrent" $(QTDIR)\bin\moc.exe;%(FullPath) - Moc%27ing iso_integrity.h... + Moc%27ing content_integrity.h... .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DNDEBUG -DQT_CONCURRENT_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtConcurrent" diff --git a/rpcs3/rpcs3.vcxproj.filters b/rpcs3/rpcs3.vcxproj.filters index 184069c388..0ad2d1d68e 100644 --- a/rpcs3/rpcs3.vcxproj.filters +++ b/rpcs3/rpcs3.vcxproj.filters @@ -1302,13 +1302,13 @@ Gui\settings - + Generated Files\Debug - + Generated Files\Release - + Gui\game list @@ -2077,7 +2077,7 @@ - + Gui\game list diff --git a/rpcs3/rpcs3qt/CMakeLists.txt b/rpcs3/rpcs3qt/CMakeLists.txt index 849e162a86..27dc3af33d 100644 --- a/rpcs3/rpcs3qt/CMakeLists.txt +++ b/rpcs3/rpcs3qt/CMakeLists.txt @@ -12,6 +12,7 @@ add_library(rpcs3_ui STATIC config_adapter.cpp config_checker.cpp config_database.cpp + content_integrity.cpp curl_handle.cpp custom_dialog.cpp custom_table_widget_item.cpp @@ -50,7 +51,6 @@ add_library(rpcs3_ui STATIC input_dialog.cpp instruction_editor_dialog.cpp ipc_settings_dialog.cpp - iso_integrity.cpp kamen_rider_dialog.cpp kernel_explorer.cpp localized.cpp diff --git a/rpcs3/rpcs3qt/content_integrity.cpp b/rpcs3/rpcs3qt/content_integrity.cpp new file mode 100644 index 0000000000..5b89c6ff37 --- /dev/null +++ b/rpcs3/rpcs3qt/content_integrity.cpp @@ -0,0 +1,136 @@ +#include "content_integrity.h" +#include "gui_settings.h" + +#include "Emu/system_utils.hpp" + +#include +#include + +LOG_CHANNEL(sys_log, "INTEGRITY"); + +content_integrity::content_integrity(QWidget* parent, content_file_type file_type) + : QObject(parent), m_file_type(file_type) +{ + switch (m_file_type) + { + case content_file_type::ISO: + m_file_path = QString::fromStdString(rpcs3::utils::get_redump_db_path()); + m_url = rpcs3::utils::get_redump_db_download_url(); + m_data_prefix = "redump"; + break; + case content_file_type::PSN_CONTENT: + m_file_path = QString::fromStdString(rpcs3::utils::get_psn_content_db_path()); + m_url = rpcs3::utils::get_psn_content_db_download_url(); + m_data_prefix = "psn_content"; + break; + case content_file_type::PSN_DLC: + m_file_path = QString::fromStdString(rpcs3::utils::get_psn_dlc_db_path()); + m_url = rpcs3::utils::get_psn_dlc_db_download_url(); + m_data_prefix = "psn_dlc"; + break; + case content_file_type::PSN_UPDATE: + m_file_path = QString::fromStdString(rpcs3::utils::get_psn_update_db_path()); + m_url = rpcs3::utils::get_psn_update_db_download_url(); + m_data_prefix = "psn_update"; + break; + default: // Let the further download attempt fail and log the error message + break; + } + + m_downloader = new downloader(parent); + + connect(m_downloader, &downloader::signal_download_finished, this, &content_integrity::handle_download_finished); + connect(m_downloader, &downloader::signal_download_canceled, this, &content_integrity::handle_download_canceled); + connect(m_downloader, &downloader::signal_download_error, this, &content_integrity::handle_download_error); +} + +void content_integrity::download() +{ + sys_log.notice("Starting database download from: %s", m_url); + + m_downloader->start(m_url, true, true, true, tr("Downloading database")); +} + +void content_integrity::handle_download_finished(const QByteArray& content) +{ + sys_log.notice("Database download finished"); + + // Write database to file + if (QByteArray data = read_json(content, true); !data.isEmpty()) + { + QFile file(m_file_path); + + if (file.exists()) + { + sys_log.notice("Database file found: %s", m_file_path); + } + + if (!file.open(QIODevice::WriteOnly)) + { + sys_log.error("Failed to write database to file: %s", m_file_path); + return; + } + + file.write(data); + file.close(); + + sys_log.success("Database written to file: %s", m_file_path); + } +} + +void content_integrity::handle_download_canceled() +{ + sys_log.notice("Database download canceled"); +} + +void content_integrity::handle_download_error(const QString& error) +{ + sys_log.error("", error.toStdString().c_str()); +} + +QByteArray content_integrity::read_json(const QByteArray& data, bool after_download) +{ + QJsonParseError error{}; + const QJsonDocument json_document = QJsonDocument::fromJson(data, &error); + + if (!json_document.isObject()) + { + sys_log.error("Integrity database error - Invalid JSON: '%s'", error.errorString()); + return {}; + } + + const QJsonObject json_data = json_document.object(); + const int return_code = json_data["return_code"].toInt(-255); + + if (return_code < 0) + { + if (after_download) + { + std::string error_message; + + switch (return_code) + { + case -1: error_message = "Server Error - Internal Error"; break; + case -2: error_message = "Server Error - Maintenance Mode"; break; + case -255: error_message = "Server Error - Return code not found"; break; + default: error_message = "Server Error - Unknown Error"; break; + } + + sys_log.error("%s: return code %d", error_message, return_code); + } + else + { + sys_log.error("Integrity database error - Invalid: return code %d", return_code); + } + + return {}; + } + + if (!json_data[m_data_prefix.c_str()].isString()) + { + sys_log.error("Integrity database error - Unusable string"); + return {}; + } + + return QByteArray().fromStdString(json_data[m_data_prefix.c_str()].toString().toStdString()); +}; diff --git a/rpcs3/rpcs3qt/iso_integrity.h b/rpcs3/rpcs3qt/content_integrity.h similarity index 57% rename from rpcs3/rpcs3qt/iso_integrity.h rename to rpcs3/rpcs3qt/content_integrity.h index a4225770de..d7d51b9a55 100644 --- a/rpcs3/rpcs3qt/iso_integrity.h +++ b/rpcs3/rpcs3qt/content_integrity.h @@ -2,13 +2,15 @@ #include "downloader.h" -class iso_integrity : public QObject +#include "Loader/content_validation.h" + +class content_integrity : public QObject { Q_OBJECT public: - // Handles download for the ISO integrity database - iso_integrity(QWidget* parent); + // Handles download for the content integrity database + content_integrity(QWidget* parent, content_file_type file_type); // Downloads and writes the database to file void download(); @@ -21,6 +23,9 @@ private Q_SLOTS: private: QByteArray read_json(const QByteArray& data, bool after_download); - QString m_filepath; + content_file_type m_file_type; + QString m_file_path; + std::string m_url; + std::string m_data_prefix; downloader* m_downloader = nullptr; }; diff --git a/rpcs3/rpcs3qt/game_list_actions.cpp b/rpcs3/rpcs3qt/game_list_actions.cpp index 13e9d8d29d..96690c742f 100644 --- a/rpcs3/rpcs3qt/game_list_actions.cpp +++ b/rpcs3/rpcs3qt/game_list_actions.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -367,17 +368,43 @@ void game_list_actions::ShowGameInfoDialog(const std::vector& games) QMessageBox::information(m_game_list_frame, tr("Game Info"), GetContentInfo(games).info); } -void game_list_actions::ShowGameIntegrityDialog(const game_info& game) +void game_list_actions::ShowGameIntegrityDialog(content_file_type file_type, const game_info& game) { if (m_game_integrity_future.isRunning()) // Still running the last request return; + const QString path_last_pkg = m_gui_settings->GetValue(gui::fd_install_pkg).toString(); + QString path; + // Initialize the validator (set also file size etc.) - m_iso_validator->init_hash(game->info.path); + switch (file_type) + { + case content_file_type::ISO: + path = QString::fromStdString(game->info.path); + break; + case content_file_type::PSN_CONTENT: + case content_file_type::PSN_DLC: + path = QFileDialog::getOpenFileName(nullptr, tr("Select package or rap file to check"), + path_last_pkg, tr("All relevant (*.pkg *.PKG *.rap *.RAP *.edat *.EDAT);;Package files (*.pkg *.PKG);;Rap files (*.rap *.RAP);;Edat files (*.edat *.EDAT);;All files (*.*)")); + break; + case content_file_type::PSN_UPDATE: + path = QFileDialog::getOpenFileName(nullptr, tr("Select package to check"), + path_last_pkg, tr("All relevant (*.pkg *.PKG);;Package files (*.pkg *.PKG);;All files (*.*)")); + break; + default: // Let the following check on path length fail and exit + break; + } + + if (path.isEmpty()) + { + return; + } + + m_game_validator->init_hash(path.toStdString()); // Game integrity check can take a while (in particular on non ssd/m.2 disks) // so run it on a concurrent thread avoiding to block the entire GUI - m_game_integrity_future = QtConcurrent::run([this]() + m_game_integrity_future = QtConcurrent::run([this, file_type]() { thread_base::set_name("Game Integrity"); @@ -385,7 +412,7 @@ void game_list_actions::ShowGameIntegrityDialog(const game_info& game) std::string hash, game_name; bool info_dialog = false; - if (m_iso_validator->calculate_hash(hash) != iso_hash_status::COMPLETED) + if (m_game_validator->calculate_hash(hash) != content_hash_status::COMPLETED) { text = "Hash calculation failed!\n\nIntegrity check aborted"; } @@ -393,13 +420,13 @@ void game_list_actions::ShowGameIntegrityDialog(const game_info& game) { text = "Integrity check completed!\n\n"; - switch (m_iso_validator->check_integrity(hash, &game_name)) + switch (m_game_validator->check_integrity(file_type, hash, &game_name)) { - case iso_integrity_status::NO_MATCH: + case content_integrity_status::NO_MATCH: text += tr("Game check NOT PASSED\n\nNo match found on DB or game corrupted:\n - Hash: %0") .arg(QString::fromStdString(hash)); break; - case iso_integrity_status::FOUND_MATCH: + case content_integrity_status::FOUND_MATCH: text += tr("Game check PASSED\n\nMatch found on DB:\n - Game: %0\n - Hash: %1") .arg(QString::fromStdString(game_name)) .arg(QString::fromStdString(hash)); @@ -427,7 +454,7 @@ void game_list_actions::ShowGameIntegrityDialog(const game_info& game) }, nullptr, false); }); - progress_dialog* pdlg = new progress_dialog(tr("ISO File Hash Calculation"), tr("Calculating hash"), tr("Cancel"), + progress_dialog* pdlg = new progress_dialog(tr("File Hash Calculation"), tr("Calculating hash"), tr("Cancel"), 0, 100, false, m_game_list_frame); pdlg->setAutoClose(false); @@ -436,18 +463,18 @@ void game_list_actions::ShowGameIntegrityDialog(const game_info& game) connect(pdlg, &progress_dialog::canceled, m_game_list_frame, [this]() { - m_iso_validator->abort_hash(); + m_game_validator->abort_hash(); }); QTimer* update_timer = new QTimer(m_game_list_frame); connect(update_timer, &QTimer::timeout, m_game_list_frame, [this, pdlg, update_timer]() { - if (m_iso_validator->get_status() == iso_hash_status::INITIALIZED) + if (m_game_validator->get_status() == content_hash_status::INITIALIZED) { // Set progress in range 0-100 - const int progress = m_iso_validator->get_size() ? - (static_cast(m_iso_validator->get_bytes_read()) / m_iso_validator->get_size()) * 100 : + const int progress = m_game_validator->get_size() ? + (static_cast(m_game_validator->get_bytes_read()) / m_game_validator->get_size()) * 100 : 0; pdlg->setValue(progress); diff --git a/rpcs3/rpcs3qt/game_list_actions.h b/rpcs3/rpcs3qt/game_list_actions.h index 0b9fa8f8ac..ca3f3a7a4f 100644 --- a/rpcs3/rpcs3qt/game_list_actions.h +++ b/rpcs3/rpcs3qt/game_list_actions.h @@ -2,7 +2,7 @@ #include "gui_game_info.h" #include "shortcut_utils.h" -#include "Loader/iso_validation.h" +#include "Loader/content_validation.h" #include #include @@ -55,7 +55,7 @@ public: void ShowRemoveGameDialog(const std::vector& games); void ShowGameInfoDialog(const std::vector& games); - void ShowGameIntegrityDialog(const game_info& game); + void ShowGameIntegrityDialog(content_file_type file_type, const game_info& game); void ShowDiskUsageDialog(); // NOTES: @@ -100,7 +100,7 @@ private: std::shared_ptr m_gui_settings; QFuture m_disk_usage_future; QFuture m_game_integrity_future; - std::shared_ptr m_iso_validator = std::make_shared(); + std::shared_ptr m_game_validator = std::make_shared(); // NOTE: // m_content_info is used by: diff --git a/rpcs3/rpcs3qt/game_list_context_menu.cpp b/rpcs3/rpcs3qt/game_list_context_menu.cpp index eb56a76ab1..53a8cd03af 100644 --- a/rpcs3/rpcs3qt/game_list_context_menu.cpp +++ b/rpcs3/rpcs3qt/game_list_context_menu.cpp @@ -17,7 +17,7 @@ #include "Utilities/File.h" #include "Emu/system_utils.hpp" #include "Loader/ISO.h" -#include "Loader/iso_validation.h" +#include "Loader/content_validation.h" #include "QApplication" #include "QClipboard" @@ -602,7 +602,10 @@ void game_list_context_menu::show_single_selection_context_menu(const game_info& addSeparator(); - // Check integrity + // Check Integrity menu + QMenu* check_integrity_menu = addMenu(tr("&Check Integrity")); + + // Check disc game integrity if (QString::fromStdString(current_game.category) == cat::cat_disc_game) { std::string key_path; @@ -613,30 +616,80 @@ void game_list_context_menu::show_single_selection_context_menu(const game_info& // That is to highlight a Redump ISO from a non Redump ISO if (iso_type != iso_type_status::NOT_ISO) { - const iso_integrity_status iso_integrity = iso_file_validation::check_integrity(""); + const content_integrity_status iso_integrity = content_validation::check_integrity(content_file_type::ISO, ""); - QAction* check_integrity = addAction(tr("&Check ISO Integrity")); + QAction* check_iso = check_integrity_menu->addAction(tr("&Check ISO Integrity")); // If it's a Redump ISO and the integrity DB exists - if (iso_type == iso_type_status::REDUMP_ISO && iso_integrity != iso_integrity_status::ERROR_OPENING_DB) + if (iso_type == iso_type_status::REDUMP_ISO && iso_integrity != content_integrity_status::ERROR_OPENING_DB) { - connect(check_integrity, &QAction::triggered, this, [this, gameinfo]() + connect(check_iso, &QAction::triggered, this, [this, gameinfo]() { - m_game_list_actions->ShowGameIntegrityDialog(gameinfo); + m_game_list_actions->ShowGameIntegrityDialog(content_file_type::ISO, gameinfo); }); } else { - check_integrity->setEnabled(false); + check_iso->setEnabled(false); } - - QAction* download_integrity = addAction(tr("&Download Integrity Database")); - connect(download_integrity, &QAction::triggered, m_game_list_frame, [this] - { - ensure(m_game_list_frame->GetIsoIntegrity())->download(); - }); } } + else // Check HDD game integrity + { + QAction* check_psn_content = check_integrity_menu->addAction(tr("&Check Game Integrity")); + + // If the integrity DB exists + if (content_validation::check_integrity(content_file_type::PSN_CONTENT, "") != content_integrity_status::ERROR_OPENING_DB) + { + connect(check_psn_content, &QAction::triggered, this, [this, gameinfo]() + { + m_game_list_actions->ShowGameIntegrityDialog(content_file_type::PSN_CONTENT, gameinfo); + }); + } + else + { + check_psn_content->setEnabled(false); + } + } + + QAction* check_psn_dlc = check_integrity_menu->addAction(tr("&Check DLC Integrity")); + + // If the integrity DB exists + if (content_validation::check_integrity(content_file_type::PSN_DLC, "") != content_integrity_status::ERROR_OPENING_DB) + { + connect(check_psn_dlc, &QAction::triggered, this, [this, gameinfo]() + { + m_game_list_actions->ShowGameIntegrityDialog(content_file_type::PSN_DLC, gameinfo); + }); + } + else + { + check_psn_dlc->setEnabled(false); + } + + QAction* check_psn_update = check_integrity_menu->addAction(tr("&Check Update Integrity")); + + // If the integrity DB exists + if (content_validation::check_integrity(content_file_type::PSN_UPDATE, "") != content_integrity_status::ERROR_OPENING_DB) + { + connect(check_psn_update, &QAction::triggered, this, [this, gameinfo]() + { + m_game_list_actions->ShowGameIntegrityDialog(content_file_type::PSN_UPDATE, gameinfo); + }); + } + else + { + check_psn_update->setEnabled(false); + } + + QAction* download_integrity = addAction(tr("&Download Integrity Databases")); + connect(download_integrity, &QAction::triggered, m_game_list_frame, [this] + { + ensure(m_game_list_frame->GetIsoIntegrity())->download(); + ensure(m_game_list_frame->GetPsnContentIntegrity())->download(); + ensure(m_game_list_frame->GetPsnDlcIntegrity())->download(); + ensure(m_game_list_frame->GetPsnUpdateIntegrity())->download(); + }); QAction* check_compat = addAction(tr("&Check Game Compatibility")); QAction* download_compat = addAction(tr("&Download Compatibility Database")); diff --git a/rpcs3/rpcs3qt/game_list_frame.cpp b/rpcs3/rpcs3qt/game_list_frame.cpp index 6e745f448c..5371aaa44e 100644 --- a/rpcs3/rpcs3qt/game_list_frame.cpp +++ b/rpcs3/rpcs3qt/game_list_frame.cpp @@ -74,7 +74,10 @@ game_list_frame::game_list_frame(std::shared_ptr gui_settings, std m_game_list->installEventFilter(this); m_game_list->verticalScrollBar()->installEventFilter(this); - m_iso_integrity = new iso_integrity(this); + m_iso_integrity = new content_integrity(this, content_file_type::ISO); + m_psn_content_integrity = new content_integrity(this, content_file_type::PSN_CONTENT); + m_psn_dlc_integrity = new content_integrity(this, content_file_type::PSN_DLC); + m_psn_update_integrity = new content_integrity(this, content_file_type::PSN_UPDATE); m_game_compat = new game_compatibility(this); m_config_db = new config_database(this); diff --git a/rpcs3/rpcs3qt/game_list_frame.h b/rpcs3/rpcs3qt/game_list_frame.h index 55569aa950..0659b350c8 100644 --- a/rpcs3/rpcs3qt/game_list_frame.h +++ b/rpcs3/rpcs3qt/game_list_frame.h @@ -3,7 +3,7 @@ #include "game_list.h" #include "game_list_actions.h" #include "custom_dock_widget.h" -#include "iso_integrity.h" +#include "content_integrity.h" #include "Utilities/lockless.h" #include "Utilities/mutex.h" @@ -56,7 +56,10 @@ public: void SetShowHidden(bool show); - iso_integrity* GetIsoIntegrity() const { return m_iso_integrity; } + content_integrity* GetIsoIntegrity() const { return m_iso_integrity; } + content_integrity* GetPsnContentIntegrity() const { return m_psn_content_integrity; } + content_integrity* GetPsnDlcIntegrity() const { return m_psn_dlc_integrity; } + content_integrity* GetPsnUpdateIntegrity() const { return m_psn_update_integrity; } game_compatibility* GetGameCompatibility() const { return ensure(m_game_compat); } config_database* GetConfigDatabase() const { return ensure(m_config_db); } const std::vector& GetGameInfo() const { return m_game_data; } @@ -156,7 +159,10 @@ private: // Game List game_list_table* m_game_list = nullptr; - iso_integrity* m_iso_integrity = nullptr; + content_integrity* m_iso_integrity = nullptr; + content_integrity* m_psn_content_integrity = nullptr; + content_integrity* m_psn_dlc_integrity = nullptr; + content_integrity* m_psn_update_integrity = nullptr; game_compatibility* m_game_compat = nullptr; config_database* m_config_db = nullptr; progress_dialog* m_progress_dialog = nullptr; diff --git a/rpcs3/rpcs3qt/iso_integrity.cpp b/rpcs3/rpcs3qt/iso_integrity.cpp deleted file mode 100644 index ee0f433653..0000000000 --- a/rpcs3/rpcs3qt/iso_integrity.cpp +++ /dev/null @@ -1,113 +0,0 @@ -#include "iso_integrity.h" -#include "gui_settings.h" - -#include "Emu/system_utils.hpp" - -#include -#include - -LOG_CHANNEL(iso_log, "ISO"); - -iso_integrity::iso_integrity(QWidget* parent) - : QObject(parent) -{ - m_filepath = QString::fromStdString(rpcs3::utils::get_redump_db_path()); - m_downloader = new downloader(parent); - - connect(m_downloader, &downloader::signal_download_finished, this, &iso_integrity::handle_download_finished); - connect(m_downloader, &downloader::signal_download_canceled, this, &iso_integrity::handle_download_canceled); - connect(m_downloader, &downloader::signal_download_error, this, &iso_integrity::handle_download_error); -} - -void iso_integrity::download() -{ - const std::string url = rpcs3::utils::get_redump_db_download_url(); - - iso_log.notice("Starting database download from: %s", url); - - m_downloader->start(url, true, true, true, tr("Downloading database")); -} - -void iso_integrity::handle_download_finished(const QByteArray& content) -{ - iso_log.notice("Database download finished"); - - // Write database to file - if (QByteArray data = read_json(content, true); !data.isEmpty()) - { - QFile file(m_filepath); - - if (file.exists()) - { - iso_log.notice("Database file found: %s", m_filepath); - } - - if (!file.open(QIODevice::WriteOnly)) - { - iso_log.error("Failed to write database to file: %s", m_filepath); - return; - } - - file.write(data); - file.close(); - - iso_log.success("Database written to file: %s", m_filepath); - } -} - -void iso_integrity::handle_download_canceled() -{ - iso_log.notice("Database download canceled"); -} - -void iso_integrity::handle_download_error(const QString& error) -{ - iso_log.error("", error.toStdString().c_str()); -} - -QByteArray iso_integrity::read_json(const QByteArray& data, bool after_download) -{ - QJsonParseError error{}; - const QJsonDocument json_document = QJsonDocument::fromJson(data, &error); - - if (!json_document.isObject()) - { - iso_log.error("ISO Integrity database error - Invalid JSON: '%s'", error.errorString()); - return {}; - } - - const QJsonObject json_data = json_document.object(); - const int return_code = json_data["return_code"].toInt(-255); - - if (return_code < 0) - { - if (after_download) - { - std::string error_message; - - switch (return_code) - { - case -1: error_message = "Server Error - Internal Error"; break; - case -2: error_message = "Server Error - Maintenance Mode"; break; - case -255: error_message = "Server Error - Return code not found"; break; - default: error_message = "Server Error - Unknown Error"; break; - } - - iso_log.error("%s: return code %d", error_message, return_code); - } - else - { - iso_log.error("ISO Integrity database error - Invalid: return code %d", return_code); - } - - return {}; - } - - if (!json_data["redump"].isString()) - { - iso_log.error("ISO Integrity database error - Unusable Redump string"); - return {}; - } - - return QByteArray().fromStdString(json_data["redump"].toString().toStdString()); -}; From 07fcff5e12131e53aa2c1195c64268dcf83e941f Mon Sep 17 00:00:00 2001 From: digant73 Date: Sat, 2 May 2026 03:03:21 +0200 Subject: [PATCH 2/9] apply review changes + fix bytes_to_hex() function --- Utilities/File.cpp | 42 +++++++----------------- rpcs3/Crypto/utils.cpp | 8 +++++ rpcs3/Loader/content_validation.cpp | 5 +-- rpcs3/rpcs3qt/content_integrity.cpp | 2 -- rpcs3/rpcs3qt/game_list_context_menu.cpp | 6 ++-- 5 files changed, 26 insertions(+), 37 deletions(-) diff --git a/Utilities/File.cpp b/Utilities/File.cpp index da29fba7bd..8fda5c171a 100644 --- a/Utilities/File.cpp +++ b/Utilities/File.cpp @@ -1125,13 +1125,6 @@ bool fs::is_optical_raw_device([[maybe_unused]] const std::string& path) bool fs::get_optical_raw_device(const std::string& path, std::string* raw_device) { - // Skip a useless check to detect an optical raw device if navigating on subfolders (e.g. C:/subfolder_1/subfolder_2/), it means we are on a hdd/ssd. - // A path for an optical drive should include only the drive letter (e.g. E:/) - if (path.find_first_of(":") != path.find_last_not_of(delim)) - { - return false; - } - if (fs::is_optical_raw_device(path)) { if (raw_device) @@ -1143,38 +1136,27 @@ bool fs::get_optical_raw_device(const std::string& path, std::string* raw_device } #ifdef _WIN32 - constexpr u32 BUF_SIZE = 1000; - WCHAR drive_list[BUF_SIZE] = {0}; + // Skip a useless check to detect an optical raw device if navigating on subfolders (e.g. C:\subfolder_1\subfolder_2\), + // it means we are on a HDD/SSD. A path for an optical drive should include only the drive letter (e.g. E:\) + const size_t drive_delim = path.find_first_of(":"); - // GetLogicalDriveStrings() returns a double-null terminated list of null-terminated strings. - // E.g. A:\B:\C:\ - const DWORD copied = GetLogicalDriveStrings(BUF_SIZE, drive_list); - - if (copied == 0 || copied > BUF_SIZE) + if (drive_delim != umax && drive_delim != path.find_last_not_of(delim)) { return false; } - for (const WCHAR* drive = drive_list; drive && *drive; drive += wcslen(drive) + 1) + const std::string drive_letter = path.substr(0, drive_delim + 1); // e.g. "E:" + const std::string drive_path = drive_letter + "\\"; // e.g. "E:\" + + if (GetDriveTypeA(drive_path.c_str()) == DRIVE_CDROM) { - if (GetDriveType(drive) == DRIVE_CDROM) + if (raw_device) { - const std::wstring ws(drive); - const std::string s = std::string(ws.begin(), ws.end() - 1); - - if (path.starts_with(s)) - { - if (raw_device) - { - *raw_device = "\\\\.\\" + s; - } - - return true; - } + *raw_device = "\\\\.\\" + drive_letter; } - } - return false; + return true; + } #endif return false; } diff --git a/rpcs3/Crypto/utils.cpp b/rpcs3/Crypto/utils.cpp index 51ad284c62..7c1d1df309 100644 --- a/rpcs3/Crypto/utils.cpp +++ b/rpcs3/Crypto/utils.cpp @@ -37,6 +37,14 @@ void bytes_to_hex(std::string& hex_str, const unsigned char* data, unsigned int { fmt::throw_exception("Failed to read bytes: %s", std::make_error_code(err).message()); } + + // Padding handling for values ​​< 0x10 (e.g. 0x05 becomes "5" instead of "05") + // If to_chars only writes 1 character, we move to the right and put '0' + if (ptr == &hex_str[i] + 1) + { + hex_str[i + 1] = hex_str[i]; + hex_str[i] = '0'; + } } } diff --git a/rpcs3/Loader/content_validation.cpp b/rpcs3/Loader/content_validation.cpp index 5bf9bbe072..bcd419b094 100644 --- a/rpcs3/Loader/content_validation.cpp +++ b/rpcs3/Loader/content_validation.cpp @@ -32,8 +32,6 @@ content_integrity_status content_validation::check_integrity(content_file_type f case content_file_type::PSN_UPDATE: db_path = rpcs3::utils::get_psn_update_db_path(); break; - default: // Let the following opening attempt fail and log the error message - break; } fs::file db_file(db_path); @@ -63,6 +61,9 @@ content_integrity_status content_validation::check_integrity(content_file_type f return content_integrity_status::ERROR_PARSING_DB; } + // Close the file and work with the data loaded into the "db" document + db_file.close(); + std::shared_ptr db_base = db.GetRoot(); if (!db_base) diff --git a/rpcs3/rpcs3qt/content_integrity.cpp b/rpcs3/rpcs3qt/content_integrity.cpp index 5b89c6ff37..13768ef1dd 100644 --- a/rpcs3/rpcs3qt/content_integrity.cpp +++ b/rpcs3/rpcs3qt/content_integrity.cpp @@ -33,8 +33,6 @@ content_integrity::content_integrity(QWidget* parent, content_file_type file_typ m_url = rpcs3::utils::get_psn_update_db_download_url(); m_data_prefix = "psn_update"; break; - default: // Let the further download attempt fail and log the error message - break; } m_downloader = new downloader(parent); diff --git a/rpcs3/rpcs3qt/game_list_context_menu.cpp b/rpcs3/rpcs3qt/game_list_context_menu.cpp index 53a8cd03af..3e1d02df67 100644 --- a/rpcs3/rpcs3qt/game_list_context_menu.cpp +++ b/rpcs3/rpcs3qt/game_list_context_menu.cpp @@ -636,7 +636,7 @@ void game_list_context_menu::show_single_selection_context_menu(const game_info& } else // Check HDD game integrity { - QAction* check_psn_content = check_integrity_menu->addAction(tr("&Check Game Integrity")); + QAction* check_psn_content = check_integrity_menu->addAction(tr("&Check Game PKG Integrity")); // If the integrity DB exists if (content_validation::check_integrity(content_file_type::PSN_CONTENT, "") != content_integrity_status::ERROR_OPENING_DB) @@ -652,7 +652,7 @@ void game_list_context_menu::show_single_selection_context_menu(const game_info& } } - QAction* check_psn_dlc = check_integrity_menu->addAction(tr("&Check DLC Integrity")); + QAction* check_psn_dlc = check_integrity_menu->addAction(tr("&Check DLC PKG Integrity")); // If the integrity DB exists if (content_validation::check_integrity(content_file_type::PSN_DLC, "") != content_integrity_status::ERROR_OPENING_DB) @@ -667,7 +667,7 @@ void game_list_context_menu::show_single_selection_context_menu(const game_info& check_psn_dlc->setEnabled(false); } - QAction* check_psn_update = check_integrity_menu->addAction(tr("&Check Update Integrity")); + QAction* check_psn_update = check_integrity_menu->addAction(tr("&Check Update PKG Integrity")); // If the integrity DB exists if (content_validation::check_integrity(content_file_type::PSN_UPDATE, "") != content_integrity_status::ERROR_OPENING_DB) From 564829613a7de0fd05ad54f26012711b30f3638f Mon Sep 17 00:00:00 2001 From: digant73 Date: Sun, 3 May 2026 16:05:48 +0200 Subject: [PATCH 3/9] auto-detect pkg type and apply related DB --- rpcs3/rpcs3qt/game_list_actions.cpp | 56 ++++++++++++++++-------- rpcs3/rpcs3qt/game_list_context_menu.cpp | 51 +++------------------ 2 files changed, 44 insertions(+), 63 deletions(-) diff --git a/rpcs3/rpcs3qt/game_list_actions.cpp b/rpcs3/rpcs3qt/game_list_actions.cpp index 96690c742f..c4da588f06 100644 --- a/rpcs3/rpcs3qt/game_list_actions.cpp +++ b/rpcs3/rpcs3qt/game_list_actions.cpp @@ -373,38 +373,53 @@ void game_list_actions::ShowGameIntegrityDialog(content_file_type file_type, con if (m_game_integrity_future.isRunning()) // Still running the last request return; - const QString path_last_pkg = m_gui_settings->GetValue(gui::fd_install_pkg).toString(); QString path; + std::string db_id; - // Initialize the validator (set also file size etc.) switch (file_type) { case content_file_type::ISO: path = QString::fromStdString(game->info.path); + db_id = "REDUMP"; break; - case content_file_type::PSN_CONTENT: - case content_file_type::PSN_DLC: + default: // Auto-detect the file type + const QString path_last_pkg = m_gui_settings->GetValue(gui::fd_install_pkg).toString(); + path = QFileDialog::getOpenFileName(nullptr, tr("Select package or rap file to check"), path_last_pkg, tr("All relevant (*.pkg *.PKG *.rap *.RAP *.edat *.EDAT);;Package files (*.pkg *.PKG);;Rap files (*.rap *.RAP);;Edat files (*.edat *.EDAT);;All files (*.*)")); - break; - case content_file_type::PSN_UPDATE: - path = QFileDialog::getOpenFileName(nullptr, tr("Select package to check"), - path_last_pkg, tr("All relevant (*.pkg *.PKG);;Package files (*.pkg *.PKG);;All files (*.*)")); - break; - default: // Let the following check on path length fail and exit + + if (path.isEmpty()) + { + return; + } + + compat::package_info info = game_compatibility::GetPkgInfo(path, m_game_list_frame->GetGameCompatibility()); + + switch (info.type) + { + case compat::package_type::update: + file_type = content_file_type::PSN_UPDATE; + db_id = "PSN UPDATE"; + break; + case compat::package_type::dlc: + file_type = content_file_type::PSN_DLC; + db_id = "PSN DLC"; + break; + case compat::package_type::other: + file_type = content_file_type::PSN_CONTENT; + db_id = "PSN CONTENT"; + break; + } + break; } - if (path.isEmpty()) - { - return; - } - + // Initialize the validator (set also file size etc.) m_game_validator->init_hash(path.toStdString()); // Game integrity check can take a while (in particular on non ssd/m.2 disks) // so run it on a concurrent thread avoiding to block the entire GUI - m_game_integrity_future = QtConcurrent::run([this, file_type]() + m_game_integrity_future = QtConcurrent::run([this, file_type, db_id]() { thread_base::set_name("Game Integrity"); @@ -423,18 +438,21 @@ void game_list_actions::ShowGameIntegrityDialog(content_file_type file_type, con switch (m_game_validator->check_integrity(file_type, hash, &game_name)) { case content_integrity_status::NO_MATCH: - text += tr("Game check NOT PASSED\n\nNo match found on DB or game corrupted:\n - Hash: %0") + text += tr("Game check NOT PASSED\n\nNo match found on '%0' DB or game corrupted:\n - Hash: %1") + .arg(QString::fromStdString(db_id)) .arg(QString::fromStdString(hash)); break; case content_integrity_status::FOUND_MATCH: - text += tr("Game check PASSED\n\nMatch found on DB:\n - Game: %0\n - Hash: %1") + text += tr("Game check PASSED\n\nMatch found on '%0' DB:\n - Game: %1\n - Hash: %2") + .arg(QString::fromStdString(db_id)) .arg(QString::fromStdString(game_name)) .arg(QString::fromStdString(hash)); info_dialog = true; break; default: - text += tr("Error parsing DB"); + text += tr("Error parsing '%0' DB or DB not existing") + .arg(QString::fromStdString(db_id)); break; } } diff --git a/rpcs3/rpcs3qt/game_list_context_menu.cpp b/rpcs3/rpcs3qt/game_list_context_menu.cpp index 3e1d02df67..c134c53115 100644 --- a/rpcs3/rpcs3qt/game_list_context_menu.cpp +++ b/rpcs3/rpcs3qt/game_list_context_menu.cpp @@ -634,53 +634,16 @@ void game_list_context_menu::show_single_selection_context_menu(const game_info& } } } - else // Check HDD game integrity - { - QAction* check_psn_content = check_integrity_menu->addAction(tr("&Check Game PKG Integrity")); - // If the integrity DB exists - if (content_validation::check_integrity(content_file_type::PSN_CONTENT, "") != content_integrity_status::ERROR_OPENING_DB) - { - connect(check_psn_content, &QAction::triggered, this, [this, gameinfo]() - { - m_game_list_actions->ShowGameIntegrityDialog(content_file_type::PSN_CONTENT, gameinfo); - }); - } - else - { - check_psn_content->setEnabled(false); - } - } + // Check integrity for the other categories based on .PKG, .RAP and .EDAT (e.g. HDD game, DLC, Update) + QAction* check_psn_content = check_integrity_menu->addAction(tr("&Check Package Integrity")); - QAction* check_psn_dlc = check_integrity_menu->addAction(tr("&Check DLC PKG Integrity")); - - // If the integrity DB exists - if (content_validation::check_integrity(content_file_type::PSN_DLC, "") != content_integrity_status::ERROR_OPENING_DB) + connect(check_psn_content, &QAction::triggered, this, [this, gameinfo]() { - connect(check_psn_dlc, &QAction::triggered, this, [this, gameinfo]() - { - m_game_list_actions->ShowGameIntegrityDialog(content_file_type::PSN_DLC, gameinfo); - }); - } - else - { - check_psn_dlc->setEnabled(false); - } - - QAction* check_psn_update = check_integrity_menu->addAction(tr("&Check Update PKG Integrity")); - - // If the integrity DB exists - if (content_validation::check_integrity(content_file_type::PSN_UPDATE, "") != content_integrity_status::ERROR_OPENING_DB) - { - connect(check_psn_update, &QAction::triggered, this, [this, gameinfo]() - { - m_game_list_actions->ShowGameIntegrityDialog(content_file_type::PSN_UPDATE, gameinfo); - }); - } - else - { - check_psn_update->setEnabled(false); - } + // File type different than ISO as passed here (PSN_CONTENT) will be properly detected in + // ShowGameIntegrityDialog() based on the selected package file + m_game_list_actions->ShowGameIntegrityDialog(content_file_type::PSN_CONTENT, gameinfo); + }); QAction* download_integrity = addAction(tr("&Download Integrity Databases")); connect(download_integrity, &QAction::triggered, m_game_list_frame, [this] From 4a985532560cd6eaf1670e2dfe529f1472d2ee0d Mon Sep 17 00:00:00 2001 From: digant73 Date: Sun, 3 May 2026 16:53:51 +0200 Subject: [PATCH 4/9] add download integrity databases entry on Multi-select context menu --- rpcs3/rpcs3qt/game_list_context_menu.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/rpcs3/rpcs3qt/game_list_context_menu.cpp b/rpcs3/rpcs3qt/game_list_context_menu.cpp index c134c53115..18914570d2 100644 --- a/rpcs3/rpcs3qt/game_list_context_menu.cpp +++ b/rpcs3/rpcs3qt/game_list_context_menu.cpp @@ -1018,6 +1018,15 @@ void game_list_context_menu::show_multi_selection_context_menu(const std::vector addSeparator(); + QAction* download_integrity = addAction(tr("&Download Integrity Databases")); + connect(download_integrity, &QAction::triggered, m_game_list_frame, [this] + { + ensure(m_game_list_frame->GetIsoIntegrity())->download(); + ensure(m_game_list_frame->GetPsnContentIntegrity())->download(); + ensure(m_game_list_frame->GetPsnDlcIntegrity())->download(); + ensure(m_game_list_frame->GetPsnUpdateIntegrity())->download(); + }); + QAction* download_compat = addAction(tr("&Download Compatibility Database")); connect(download_compat, &QAction::triggered, m_game_list_frame, [this] { From 97fba17b968bd20a5e65a31bcf492c11a03331d3 Mon Sep 17 00:00:00 2001 From: digant73 Date: Thu, 7 May 2026 23:51:23 +0200 Subject: [PATCH 5/9] Add integrity check to raw device + add multi-package integrity check --- rpcs3/Loader/content_validation.cpp | 26 ++-- rpcs3/Loader/content_validation.h | 6 + rpcs3/rpcs3qt/game_list_actions.cpp | 184 ++++++++++++++--------- rpcs3/rpcs3qt/game_list_context_menu.cpp | 8 +- 4 files changed, 143 insertions(+), 81 deletions(-) diff --git a/rpcs3/Loader/content_validation.cpp b/rpcs3/Loader/content_validation.cpp index bcd419b094..818bab63ad 100644 --- a/rpcs3/Loader/content_validation.cpp +++ b/rpcs3/Loader/content_validation.cpp @@ -1,6 +1,7 @@ #include "stdafx.h" #include "content_validation.h" +#include "ISO.h" #include "Emu/system_utils.hpp" #include "Utilities/File.h" @@ -108,18 +109,23 @@ content_integrity_status content_validation::check_integrity(content_file_type f bool content_validation::init_hash(const std::string& path) { - fs::file iso_file(path); + std::string new_path = path; - // If no ISO file exists - if (!iso_file) + fs::get_optical_raw_device(path, &new_path); + + iso_file file(new_path); + + // If no file exists + if (!file) { - sys_log.error("init_hash: Failed to open file: %s", path); + sys_log.error("init_hash: Failed to open file: %s", new_path); m_status = content_hash_status::ABORTED; return false; } - m_path = path; - m_size = iso_file.size(); + m_path = new_path; + m_name = new_path.find_last_of(fs::delim) != umax ? new_path.substr(new_path.find_last_of(fs::delim) + 1) : new_path; + m_size = file.size(); m_bytes_read = 0; m_status = content_hash_status::INITIALIZED; return true; @@ -134,10 +140,10 @@ content_hash_status content_validation::calculate_hash(std::string& hash) return m_status; } - fs::file iso_file(m_path); + iso_file file(m_path); - // If no ISO file exists - if (!iso_file) + // If no file exists + if (!file) { sys_log.error("calculate_hash: Failed to open file: %s", m_path); m_status = content_hash_status::ABORTED; @@ -154,7 +160,7 @@ content_hash_status content_validation::calculate_hash(std::string& hash) do { - bytes_read = iso_file.read(buf.data(), block_size); + bytes_read = file.read(buf.data(), block_size); mbedtls_md5_update_ret(&md5_ctx, buf.data(), bytes_read); m_bytes_read += bytes_read; diff --git a/rpcs3/Loader/content_validation.h b/rpcs3/Loader/content_validation.h index 1c20d02148..ecbb29ff79 100644 --- a/rpcs3/Loader/content_validation.h +++ b/rpcs3/Loader/content_validation.h @@ -33,17 +33,23 @@ class content_validation { private: std::string m_path; + std::string m_name; u64 m_size = 0; u64 m_bytes_read = 0; + u16 m_count = 0; // Set only by set_count() content_hash_status m_status = content_hash_status::INITIALIZED; public: static content_integrity_status check_integrity(content_file_type file_type, const std::string& hash, std::string* game_name = nullptr); const std::string& get_path() const { return m_path; } + const std::string& get_name() const { return m_name; } u64 get_size() const { return m_size; } u64 get_bytes_read() const { return m_bytes_read; } + u16 get_count() { return m_count; } content_hash_status get_status() const { return m_status; } + + void set_count(u16 count) { m_count = count; } void abort_hash() { m_status = content_hash_status::ABORTED; } bool init_hash(const std::string& path); diff --git a/rpcs3/rpcs3qt/game_list_actions.cpp b/rpcs3/rpcs3qt/game_list_actions.cpp index c4da588f06..97c5df38ab 100644 --- a/rpcs3/rpcs3qt/game_list_actions.cpp +++ b/rpcs3/rpcs3qt/game_list_actions.cpp @@ -373,106 +373,155 @@ void game_list_actions::ShowGameIntegrityDialog(content_file_type file_type, con if (m_game_integrity_future.isRunning()) // Still running the last request return; - QString path; - std::string db_id; + QStringList path_list; switch (file_type) { case content_file_type::ISO: - path = QString::fromStdString(game->info.path); - db_id = "REDUMP"; + path_list.push_back(QString::fromStdString(game->info.path)); break; default: // Auto-detect the file type const QString path_last_pkg = m_gui_settings->GetValue(gui::fd_install_pkg).toString(); - path = QFileDialog::getOpenFileName(nullptr, tr("Select package or rap file to check"), + path_list = QFileDialog::getOpenFileNames(nullptr, tr("Select package or rap file to check"), path_last_pkg, tr("All relevant (*.pkg *.PKG *.rap *.RAP *.edat *.EDAT);;Package files (*.pkg *.PKG);;Rap files (*.rap *.RAP);;Edat files (*.edat *.EDAT);;All files (*.*)")); - if (path.isEmpty()) + if (path_list.isEmpty()) { return; } - compat::package_info info = game_compatibility::GetPkgInfo(path, m_game_list_frame->GetGameCompatibility()); - - switch (info.type) - { - case compat::package_type::update: - file_type = content_file_type::PSN_UPDATE; - db_id = "PSN UPDATE"; - break; - case compat::package_type::dlc: - file_type = content_file_type::PSN_DLC; - db_id = "PSN DLC"; - break; - case compat::package_type::other: - file_type = content_file_type::PSN_CONTENT; - db_id = "PSN CONTENT"; - break; - } - break; } - // Initialize the validator (set also file size etc.) - m_game_validator->init_hash(path.toStdString()); + // Tell the progress bar thread how many hash will be checked + m_game_validator->set_count(path_list.size()); // Game integrity check can take a while (in particular on non ssd/m.2 disks) // so run it on a concurrent thread avoiding to block the entire GUI - m_game_integrity_future = QtConcurrent::run([this, file_type, db_id]() + m_game_integrity_future = QtConcurrent::run([this, type = file_type, path_list]() { thread_base::set_name("Game Integrity"); - QString text; - std::string hash, game_name; - bool info_dialog = false; + content_file_type file_type = type; + QString text_dialog, text_result; + std::string db_id, hash, game_name; + bool info_dialog = true; - if (m_game_validator->calculate_hash(hash) != content_hash_status::COMPLETED) + for (int i = 0; i < path_list.size(); i++) { - text = "Hash calculation failed!\n\nIntegrity check aborted"; - } - else - { - text = "Integrity check completed!\n\n"; + bool use_fallback_db = false; // Set to "true" only for ".rap" and ".edat" - switch (m_game_validator->check_integrity(file_type, hash, &game_name)) + if (file_type == content_file_type::ISO) { - case content_integrity_status::NO_MATCH: - text += tr("Game check NOT PASSED\n\nNo match found on '%0' DB or game corrupted:\n - Hash: %1") - .arg(QString::fromStdString(db_id)) - .arg(QString::fromStdString(hash)); - break; - case content_integrity_status::FOUND_MATCH: - text += tr("Game check PASSED\n\nMatch found on '%0' DB:\n - Game: %1\n - Hash: %2") - .arg(QString::fromStdString(db_id)) - .arg(QString::fromStdString(game_name)) - .arg(QString::fromStdString(hash)); - - info_dialog = true; - break; - default: - text += tr("Error parsing '%0' DB or DB not existing") - .arg(QString::fromStdString(db_id)); - break; - } - } - - Emu.CallFromMainThread([this, text, info_dialog]() - { - if (info_dialog) - { - sys_log.success("%s", text.toStdString()); - QMessageBox::information(m_game_list_frame, tr("Game Integrity"), text); + db_id = "REDUMP"; } else { - sys_log.error("%s", text.toStdString()); - QMessageBox::critical(m_game_list_frame, tr("Game Integrity"), text); + compat::package_info info = game_compatibility::GetPkgInfo(path_list[i], m_game_list_frame->GetGameCompatibility()); + + switch (info.type) + { + case compat::package_type::update: + file_type = content_file_type::PSN_UPDATE; + db_id = "PSN UPDATE"; + break; + case compat::package_type::dlc: + file_type = content_file_type::PSN_DLC; + db_id = "PSN DLC"; + break; + case compat::package_type::other: + // NOTE: This is also always the default type for any ".rap" and ".edat" (not possible to detect type by file parsing) + file_type = content_file_type::PSN_CONTENT; + db_id = "PSN CONTENT"; + + // If no match for ".rap" or ".edat" will be found on default "PSN Content" DB, try on "PSN DLC" DB + if (path_list[i].endsWith(".rap", Qt::CaseInsensitive) || path_list[i].endsWith(".edat", Qt::CaseInsensitive)) + { + use_fallback_db = true; + } + + break; + } + } + + // Initialize the validator (set also file size etc.) + m_game_validator->init_hash(path_list[i].toStdString()); + + if (m_game_validator->calculate_hash(hash) == content_hash_status::COMPLETED) + { + content_integrity_status integrity_status = m_game_validator->check_integrity(file_type, hash, &game_name); + + // If no match for ".rap" or ".edat" is found on default "PSN Content" DB, try on "PSN DLC" DB + if (integrity_status == content_integrity_status::NO_MATCH && use_fallback_db) + { + db_id += " -> PSN DLC"; + integrity_status = m_game_validator->check_integrity(content_file_type::PSN_DLC, hash, &game_name); + } + + switch (integrity_status) + { + case content_integrity_status::NO_MATCH: + text_result += tr("Game check NOT PASSED\n\nNo match found on '%0' DB or game corrupted:\n - File: %1\n - Hash: %2\n\n\n") + .arg(QString::fromStdString(db_id)) + .arg(QString::fromStdString(m_game_validator->get_name())) + .arg(QString::fromStdString(hash)); + + info_dialog = false; + break; + case content_integrity_status::FOUND_MATCH: + text_result += tr("Game check PASSED\n\nMatch found on '%0' DB:\n - File: %1\n - Hash: %2\n - Game: %3\n\n\n") + .arg(QString::fromStdString(db_id)) + .arg(QString::fromStdString(m_game_validator->get_name())) + .arg(QString::fromStdString(hash)) + .arg(QString::fromStdString(game_name)); + break; + default: + text_result += tr("Error parsing '%0' DB or DB not existing:\n - File: %1\n - Hash: %2\n\n\n") + .arg(QString::fromStdString(db_id)) + .arg(QString::fromStdString(m_game_validator->get_name())) + .arg(QString::fromStdString(hash)); + + info_dialog = false; + break; + } + } + + if (m_game_validator->get_status() == content_hash_status::ABORTED) + { + break; + } + } + + if (m_game_validator->get_status() == content_hash_status::ABORTED) + { + text_dialog = "Hash calculation failed!\n\nIntegrity check aborted"; + info_dialog = false; + } + else + { + text_dialog = "Integrity check completed!\n\n" + text_result; + } + + // Tell the progress bar thread to terminate + m_game_validator->set_count(0); + + Emu.CallFromMainThread([this, text_dialog, info_dialog]() + { + if (info_dialog) + { + sys_log.success("%s", text_dialog.toStdString()); + QMessageBox::information(m_game_list_frame, tr("Game Integrity"), text_dialog); + } + else + { + sys_log.error("%s", text_dialog.toStdString()); + QMessageBox::critical(m_game_list_frame, tr("Game Integrity"), text_dialog); } }, nullptr, false); }); - progress_dialog* pdlg = new progress_dialog(tr("File Hash Calculation"), tr("Calculating hash"), tr("Cancel"), + progress_dialog* pdlg = new progress_dialog(tr("File Hash Calculation"), tr(""), tr("Cancel"), 0, 100, false, m_game_list_frame); pdlg->setAutoClose(false); @@ -488,7 +537,7 @@ void game_list_actions::ShowGameIntegrityDialog(content_file_type file_type, con connect(update_timer, &QTimer::timeout, m_game_list_frame, [this, pdlg, update_timer]() { - if (m_game_validator->get_status() == content_hash_status::INITIALIZED) + if (m_game_validator->get_count()) { // Set progress in range 0-100 const int progress = m_game_validator->get_size() ? @@ -496,6 +545,7 @@ void game_list_actions::ShowGameIntegrityDialog(content_file_type file_type, con 0; pdlg->setValue(progress); + pdlg->setLabelText(tr("Calculating hash: %0").arg(m_game_validator->get_name())); } else { diff --git a/rpcs3/rpcs3qt/game_list_context_menu.cpp b/rpcs3/rpcs3qt/game_list_context_menu.cpp index 8c6696647e..4db1868b77 100644 --- a/rpcs3/rpcs3qt/game_list_context_menu.cpp +++ b/rpcs3/rpcs3qt/game_list_context_menu.cpp @@ -608,19 +608,19 @@ void game_list_context_menu::show_single_selection_context_menu(const game_info& // Check disc game integrity if (QString::fromStdString(current_game.category) == cat::cat_disc_game) { + const bool raw_archive = is_iso_file(current_game.path); const iso_type_status iso_type = iso_file_decryption::check_type(current_game.path); // If it's an ISO file (e.g. even a decrypted ISO), always provide the entry on the context menu but disable // it if the ISO does not support integrity check (e.g. non Redump ISO) or no integrity DB is found. // That is to highlight a Redump ISO from a non Redump ISO - if (iso_type != iso_type_status::NOT_ISO) + if (raw_archive || iso_type != iso_type_status::NOT_ISO) { - const content_integrity_status iso_integrity = content_validation::check_integrity(content_file_type::ISO, ""); - QAction* check_iso = check_integrity_menu->addAction(tr("&Check ISO Integrity")); // If it's a Redump ISO and the integrity DB exists - if (iso_type == iso_type_status::REDUMP_ISO && iso_integrity != content_integrity_status::ERROR_OPENING_DB) + if ((raw_archive || iso_type == iso_type_status::REDUMP_ISO) && + content_validation::check_integrity(content_file_type::ISO, "") != content_integrity_status::ERROR_OPENING_DB) { connect(check_iso, &QAction::triggered, this, [this, gameinfo]() { From 103238b8618c287e133f224b318bb3f3d93b61b4 Mon Sep 17 00:00:00 2001 From: Antonino Di Guardo <64427768+digant73@users.noreply.github.com> Date: Fri, 8 May 2026 10:30:17 +0200 Subject: [PATCH 6/9] apply review changes --- rpcs3/Loader/content_validation.h | 2 +- rpcs3/rpcs3qt/game_list_actions.cpp | 33 +++++++++++++++++------------ 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/rpcs3/Loader/content_validation.h b/rpcs3/Loader/content_validation.h index ecbb29ff79..4540ae6b3e 100644 --- a/rpcs3/Loader/content_validation.h +++ b/rpcs3/Loader/content_validation.h @@ -46,7 +46,7 @@ public: const std::string& get_name() const { return m_name; } u64 get_size() const { return m_size; } u64 get_bytes_read() const { return m_bytes_read; } - u16 get_count() { return m_count; } + u16 get_count() const { return m_count; } content_hash_status get_status() const { return m_status; } void set_count(u16 count) { m_count = count; } diff --git a/rpcs3/rpcs3qt/game_list_actions.cpp b/rpcs3/rpcs3qt/game_list_actions.cpp index 97c5df38ab..662f8aa321 100644 --- a/rpcs3/rpcs3qt/game_list_actions.cpp +++ b/rpcs3/rpcs3qt/game_list_actions.cpp @@ -404,7 +404,7 @@ void game_list_actions::ShowGameIntegrityDialog(content_file_type file_type, con thread_base::set_name("Game Integrity"); content_file_type file_type = type; - QString text_dialog, text_result; + QString text_result; std::string db_id, hash, game_name; bool info_dialog = true; @@ -416,9 +416,17 @@ void game_list_actions::ShowGameIntegrityDialog(content_file_type file_type, con { db_id = "REDUMP"; } + else if (path_list[i].endsWith(".rap", Qt::CaseInsensitive) || path_list[i].endsWith(".edat", Qt::CaseInsensitive)) + { + // NOTE: This is the default type for any ".rap" and ".edat" due to it's not possible to detect the type by file parsing. + // If no match for ".rap" or ".edat" will be found on default "PSN Content" DB, we will try on "PSN DLC" DB + file_type = content_file_type::PSN_CONTENT; + db_id = "PSN CONTENT"; + use_fallback_db = true; + } else { - compat::package_info info = game_compatibility::GetPkgInfo(path_list[i], m_game_list_frame->GetGameCompatibility()); + const compat::package_info info = game_compatibility::GetPkgInfo(path_list[i], m_game_list_frame->GetGameCompatibility()); switch (info.type) { @@ -431,16 +439,8 @@ void game_list_actions::ShowGameIntegrityDialog(content_file_type file_type, con db_id = "PSN DLC"; break; case compat::package_type::other: - // NOTE: This is also always the default type for any ".rap" and ".edat" (not possible to detect type by file parsing) file_type = content_file_type::PSN_CONTENT; db_id = "PSN CONTENT"; - - // If no match for ".rap" or ".edat" will be found on default "PSN Content" DB, try on "PSN DLC" DB - if (path_list[i].endsWith(".rap", Qt::CaseInsensitive) || path_list[i].endsWith(".edat", Qt::CaseInsensitive)) - { - use_fallback_db = true; - } - break; } } @@ -462,7 +462,7 @@ void game_list_actions::ShowGameIntegrityDialog(content_file_type file_type, con switch (integrity_status) { case content_integrity_status::NO_MATCH: - text_result += tr("Game check NOT PASSED\n\nNo match found on '%0' DB or game corrupted:\n - File: %1\n - Hash: %2\n\n\n") + text_result += tr("Game check NOT PASSED\n\nNo match found on '%0' DB or game corrupted:\n - File: %1\n - Hash: %2") .arg(QString::fromStdString(db_id)) .arg(QString::fromStdString(m_game_validator->get_name())) .arg(QString::fromStdString(hash)); @@ -470,14 +470,14 @@ void game_list_actions::ShowGameIntegrityDialog(content_file_type file_type, con info_dialog = false; break; case content_integrity_status::FOUND_MATCH: - text_result += tr("Game check PASSED\n\nMatch found on '%0' DB:\n - File: %1\n - Hash: %2\n - Game: %3\n\n\n") + text_result += tr("Game check PASSED\n\nMatch found on '%0' DB:\n - File: %1\n - Hash: %2\n - Game: %3") .arg(QString::fromStdString(db_id)) .arg(QString::fromStdString(m_game_validator->get_name())) .arg(QString::fromStdString(hash)) .arg(QString::fromStdString(game_name)); break; default: - text_result += tr("Error parsing '%0' DB or DB not existing:\n - File: %1\n - Hash: %2\n\n\n") + text_result += tr("Error parsing '%0' DB or DB not existing:\n - File: %1\n - Hash: %2") .arg(QString::fromStdString(db_id)) .arg(QString::fromStdString(m_game_validator->get_name())) .arg(QString::fromStdString(hash)); @@ -485,6 +485,11 @@ void game_list_actions::ShowGameIntegrityDialog(content_file_type file_type, con info_dialog = false; break; } + + if (i < path_list.size() - 1) // If it's not the last processed entry, add empty lines as separator + { + text_result += tr("\n\n\n"); + } } if (m_game_validator->get_status() == content_hash_status::ABORTED) @@ -493,6 +498,8 @@ void game_list_actions::ShowGameIntegrityDialog(content_file_type file_type, con } } + QString text_dialog; + if (m_game_validator->get_status() == content_hash_status::ABORTED) { text_dialog = "Hash calculation failed!\n\nIntegrity check aborted"; From c10d3a67ee86cc79acd1601debee7e89f133352b Mon Sep 17 00:00:00 2001 From: Antonino Di Guardo <64427768+digant73@users.noreply.github.com> Date: Fri, 8 May 2026 12:29:12 +0200 Subject: [PATCH 7/9] minor cleanup --- rpcs3/rpcs3qt/game_list_context_menu.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rpcs3/rpcs3qt/game_list_context_menu.cpp b/rpcs3/rpcs3qt/game_list_context_menu.cpp index 4db1868b77..0ddd583cb3 100644 --- a/rpcs3/rpcs3qt/game_list_context_menu.cpp +++ b/rpcs3/rpcs3qt/game_list_context_menu.cpp @@ -635,7 +635,7 @@ void game_list_context_menu::show_single_selection_context_menu(const game_info& } // Check integrity for the other categories based on .PKG, .RAP and .EDAT (e.g. HDD game, DLC, Update) - QAction* check_psn_content = check_integrity_menu->addAction(tr("&Check Package Integrity")); + QAction* check_psn_content = check_integrity_menu->addAction(tr("&Check Packages/Raps/Edats Integrity")); connect(check_psn_content, &QAction::triggered, this, [this, gameinfo]() { From d3c7339e9a9202f938b48bf6911913b2b6ea9a2e Mon Sep 17 00:00:00 2001 From: Antonino Di Guardo <64427768+digant73@users.noreply.github.com> Date: Fri, 8 May 2026 15:05:48 +0200 Subject: [PATCH 8/9] Update rpcs3/rpcs3qt/game_list_actions.cpp Co-authored-by: Megamouse --- rpcs3/rpcs3qt/game_list_actions.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rpcs3/rpcs3qt/game_list_actions.cpp b/rpcs3/rpcs3qt/game_list_actions.cpp index 662f8aa321..07ff1631ca 100644 --- a/rpcs3/rpcs3qt/game_list_actions.cpp +++ b/rpcs3/rpcs3qt/game_list_actions.cpp @@ -488,7 +488,7 @@ void game_list_actions::ShowGameIntegrityDialog(content_file_type file_type, con if (i < path_list.size() - 1) // If it's not the last processed entry, add empty lines as separator { - text_result += tr("\n\n\n"); + text_result += "\n\n\n"; } } From 883a2fce4278083325b5abb2abab142383281b22 Mon Sep 17 00:00:00 2001 From: Antonino Di Guardo <64427768+digant73@users.noreply.github.com> Date: Fri, 8 May 2026 15:06:22 +0200 Subject: [PATCH 9/9] Update rpcs3/rpcs3qt/game_list_actions.cpp Co-authored-by: Megamouse --- rpcs3/rpcs3qt/game_list_actions.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rpcs3/rpcs3qt/game_list_actions.cpp b/rpcs3/rpcs3qt/game_list_actions.cpp index 07ff1631ca..858af9a6af 100644 --- a/rpcs3/rpcs3qt/game_list_actions.cpp +++ b/rpcs3/rpcs3qt/game_list_actions.cpp @@ -528,7 +528,7 @@ void game_list_actions::ShowGameIntegrityDialog(content_file_type file_type, con }, nullptr, false); }); - progress_dialog* pdlg = new progress_dialog(tr("File Hash Calculation"), tr(""), tr("Cancel"), + progress_dialog* pdlg = new progress_dialog(tr("File Hash Calculation"), "", tr("Cancel"), 0, 100, false, m_game_list_frame); pdlg->setAutoClose(false);