diff --git a/rpcs3/Emu/CMakeLists.txt b/rpcs3/Emu/CMakeLists.txt index 77c8348335..de7e88be07 100644 --- a/rpcs3/Emu/CMakeLists.txt +++ b/rpcs3/Emu/CMakeLists.txt @@ -128,7 +128,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..818bab63ad --- /dev/null +++ b/rpcs3/Loader/content_validation.cpp @@ -0,0 +1,187 @@ +#include "stdafx.h" + +#include "content_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(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; + } + + 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; + } + + // 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) + { + 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) +{ + std::string new_path = path; + + 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", new_path); + m_status = content_hash_status::ABORTED; + return false; + } + + 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; +} + +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; + } + + iso_file file(m_path); + + // If no file exists + if (!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 = 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..4540ae6b3e --- /dev/null +++ b/rpcs3/Loader/content_validation.h @@ -0,0 +1,57 @@ +#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; + 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() const { 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); + 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 31ef781d93..290738d339 100644 --- a/rpcs3/emucore.vcxproj +++ b/rpcs3/emucore.vcxproj @@ -205,7 +205,7 @@ - + NotUsing @@ -780,7 +780,7 @@ - + @@ -1063,6 +1063,7 @@ + diff --git a/rpcs3/emucore.vcxproj.filters b/rpcs3/emucore.vcxproj.filters index 17ed08a3f6..9457b67838 100644 --- a/rpcs3/emucore.vcxproj.filters +++ b/rpcs3/emucore.vcxproj.filters @@ -1414,6 +1414,9 @@ Loader + + Loader + Emu\GPU\RSX\Overlays @@ -1432,7 +1435,7 @@ Emu\GPU\RSX\Overlays - + Loader @@ -2857,6 +2860,9 @@ Loader + + Loader + Emu\GPU\RSX\Overlays @@ -2881,7 +2887,7 @@ Emu\GPU\RSX\Common - + Loader diff --git a/rpcs3/rpcs3.vcxproj b/rpcs3/rpcs3.vcxproj index 61f05473b0..60d55e1bff 100644 --- a/rpcs3/rpcs3.vcxproj +++ b/rpcs3/rpcs3.vcxproj @@ -341,7 +341,7 @@ true - + true @@ -653,7 +653,7 @@ true - + true @@ -929,7 +929,7 @@ - + @@ -1924,13 +1924,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 e755ee4a67..254585c0bf 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 @@ -2083,7 +2083,7 @@ - + Gui\game list diff --git a/rpcs3/rpcs3qt/CMakeLists.txt b/rpcs3/rpcs3qt/CMakeLists.txt index 160aa4fb05..5dd092d074 100644 --- a/rpcs3/rpcs3qt/CMakeLists.txt +++ b/rpcs3/rpcs3qt/CMakeLists.txt @@ -13,6 +13,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 @@ -51,7 +52,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..13768ef1dd --- /dev/null +++ b/rpcs3/rpcs3qt/content_integrity.cpp @@ -0,0 +1,134 @@ +#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; + } + + 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..858af9a6af 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,67 +368,167 @@ 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; - // Initialize the validator (set also file size etc.) - m_iso_validator->init_hash(game->info.path); + QStringList path_list; + + switch (file_type) + { + case content_file_type::ISO: + 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_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_list.isEmpty()) + { + return; + } + + break; + } + + // 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]() + 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_result; + std::string db_id, hash, game_name; + bool info_dialog = true; - if (m_iso_validator->calculate_hash(hash) != iso_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_iso_validator->check_integrity(hash, &game_name)) + if (file_type == content_file_type::ISO) { - case iso_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: - 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)); - - info_dialog = true; - break; - default: - text += tr("Error parsing DB"); - break; + db_id = "REDUMP"; } - } - - Emu.CallFromMainThread([this, text, info_dialog]() - { - if (info_dialog) + else if (path_list[i].endsWith(".rap", Qt::CaseInsensitive) || path_list[i].endsWith(".edat", Qt::CaseInsensitive)) { - sys_log.success("%s", text.toStdString()); - QMessageBox::information(m_game_list_frame, tr("Game Integrity"), text); + // 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 { - sys_log.error("%s", text.toStdString()); - QMessageBox::critical(m_game_list_frame, tr("Game Integrity"), text); + const 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: + file_type = content_file_type::PSN_CONTENT; + db_id = "PSN CONTENT"; + 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") + .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") + .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") + .arg(QString::fromStdString(db_id)) + .arg(QString::fromStdString(m_game_validator->get_name())) + .arg(QString::fromStdString(hash)); + + 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 += "\n\n\n"; + } + } + + if (m_game_validator->get_status() == content_hash_status::ABORTED) + { + break; + } + } + + QString text_dialog; + + 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("ISO File Hash Calculation"), tr("Calculating hash"), tr("Cancel"), + progress_dialog* pdlg = new progress_dialog(tr("File Hash Calculation"), "", tr("Cancel"), 0, 100, false, m_game_list_frame); pdlg->setAutoClose(false); @@ -436,21 +537,22 @@ 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_count()) { // 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); + pdlg->setLabelText(tr("Calculating hash: %0").arg(m_game_validator->get_name())); } else { 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 402286e2f1..0ddd583cb3 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,41 +602,57 @@ 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) { + 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 iso_integrity_status iso_integrity = iso_file_validation::check_integrity(""); - - 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 ((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_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(); - }); } } + // 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 Packages/Raps/Edats Integrity")); + + connect(check_psn_content, &QAction::triggered, this, [this, gameinfo]() + { + // 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] + { + 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")); QAction* download_config_db = addAction(tr("&Download Config Database")); @@ -1001,6 +1017,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] { diff --git a/rpcs3/rpcs3qt/game_list_frame.cpp b/rpcs3/rpcs3qt/game_list_frame.cpp index 6604020b79..fa003026d1 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()); -};