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]() {