mirror of
https://github.com/RPCS3/rpcs3.git
synced 2026-05-12 16:19:44 -06:00
Add integrity check to raw device + add multi-package integrity check
This commit is contained in:
parent
835a917a42
commit
97fba17b96
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
{
|
||||
|
||||
@ -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]()
|
||||
{
|
||||
|
||||
Loading…
Reference in New Issue
Block a user