Add integrity check to raw device + add multi-package integrity check

This commit is contained in:
digant73 2026-05-07 23:51:23 +02:00
parent 835a917a42
commit 97fba17b96
4 changed files with 143 additions and 81 deletions

View File

@ -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;

View File

@ -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);

View File

@ -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
{

View File

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