mirror of
https://github.com/RPCS3/rpcs3.git
synced 2026-06-01 12:15:27 -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 "stdafx.h"
|
||||||
|
|
||||||
#include "content_validation.h"
|
#include "content_validation.h"
|
||||||
|
#include "ISO.h"
|
||||||
|
|
||||||
#include "Emu/system_utils.hpp"
|
#include "Emu/system_utils.hpp"
|
||||||
#include "Utilities/File.h"
|
#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)
|
bool content_validation::init_hash(const std::string& path)
|
||||||
{
|
{
|
||||||
fs::file iso_file(path);
|
std::string new_path = path;
|
||||||
|
|
||||||
// If no ISO file exists
|
fs::get_optical_raw_device(path, &new_path);
|
||||||
if (!iso_file)
|
|
||||||
|
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;
|
m_status = content_hash_status::ABORTED;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_path = path;
|
m_path = new_path;
|
||||||
m_size = iso_file.size();
|
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_bytes_read = 0;
|
||||||
m_status = content_hash_status::INITIALIZED;
|
m_status = content_hash_status::INITIALIZED;
|
||||||
return true;
|
return true;
|
||||||
@ -134,10 +140,10 @@ content_hash_status content_validation::calculate_hash(std::string& hash)
|
|||||||
return m_status;
|
return m_status;
|
||||||
}
|
}
|
||||||
|
|
||||||
fs::file iso_file(m_path);
|
iso_file file(m_path);
|
||||||
|
|
||||||
// If no ISO file exists
|
// If no file exists
|
||||||
if (!iso_file)
|
if (!file)
|
||||||
{
|
{
|
||||||
sys_log.error("calculate_hash: Failed to open file: %s", m_path);
|
sys_log.error("calculate_hash: Failed to open file: %s", m_path);
|
||||||
m_status = content_hash_status::ABORTED;
|
m_status = content_hash_status::ABORTED;
|
||||||
@ -154,7 +160,7 @@ content_hash_status content_validation::calculate_hash(std::string& hash)
|
|||||||
|
|
||||||
do
|
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);
|
mbedtls_md5_update_ret(&md5_ctx, buf.data(), bytes_read);
|
||||||
|
|
||||||
m_bytes_read += bytes_read;
|
m_bytes_read += bytes_read;
|
||||||
|
|||||||
@ -33,17 +33,23 @@ class content_validation
|
|||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
std::string m_path;
|
std::string m_path;
|
||||||
|
std::string m_name;
|
||||||
u64 m_size = 0;
|
u64 m_size = 0;
|
||||||
u64 m_bytes_read = 0;
|
u64 m_bytes_read = 0;
|
||||||
|
u16 m_count = 0; // Set only by set_count()
|
||||||
content_hash_status m_status = content_hash_status::INITIALIZED;
|
content_hash_status m_status = content_hash_status::INITIALIZED;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
static content_integrity_status check_integrity(content_file_type file_type, const std::string& hash, std::string* game_name = nullptr);
|
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_path() const { return m_path; }
|
||||||
|
const std::string& get_name() const { return m_name; }
|
||||||
u64 get_size() const { return m_size; }
|
u64 get_size() const { return m_size; }
|
||||||
u64 get_bytes_read() const { return m_bytes_read; }
|
u64 get_bytes_read() const { return m_bytes_read; }
|
||||||
|
u16 get_count() { return m_count; }
|
||||||
content_hash_status get_status() const { return m_status; }
|
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; }
|
void abort_hash() { m_status = content_hash_status::ABORTED; }
|
||||||
|
|
||||||
bool init_hash(const std::string& path);
|
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
|
if (m_game_integrity_future.isRunning()) // Still running the last request
|
||||||
return;
|
return;
|
||||||
|
|
||||||
QString path;
|
QStringList path_list;
|
||||||
std::string db_id;
|
|
||||||
|
|
||||||
switch (file_type)
|
switch (file_type)
|
||||||
{
|
{
|
||||||
case content_file_type::ISO:
|
case content_file_type::ISO:
|
||||||
path = QString::fromStdString(game->info.path);
|
path_list.push_back(QString::fromStdString(game->info.path));
|
||||||
db_id = "REDUMP";
|
|
||||||
break;
|
break;
|
||||||
default: // Auto-detect the file type
|
default: // Auto-detect the file type
|
||||||
const QString path_last_pkg = m_gui_settings->GetValue(gui::fd_install_pkg).toString();
|
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 (*.*)"));
|
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;
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize the validator (set also file size etc.)
|
// Tell the progress bar thread how many hash will be checked
|
||||||
m_game_validator->init_hash(path.toStdString());
|
m_game_validator->set_count(path_list.size());
|
||||||
|
|
||||||
// Game integrity check can take a while (in particular on non ssd/m.2 disks)
|
// 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
|
// 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");
|
thread_base::set_name("Game Integrity");
|
||||||
|
|
||||||
QString text;
|
content_file_type file_type = type;
|
||||||
std::string hash, game_name;
|
QString text_dialog, text_result;
|
||||||
bool info_dialog = false;
|
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";
|
bool use_fallback_db = false; // Set to "true" only for ".rap" and ".edat"
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
text = "Integrity check completed!\n\n";
|
|
||||||
|
|
||||||
switch (m_game_validator->check_integrity(file_type, hash, &game_name))
|
if (file_type == content_file_type::ISO)
|
||||||
{
|
{
|
||||||
case content_integrity_status::NO_MATCH:
|
db_id = "REDUMP";
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
sys_log.error("%s", text.toStdString());
|
compat::package_info info = game_compatibility::GetPkgInfo(path_list[i], m_game_list_frame->GetGameCompatibility());
|
||||||
QMessageBox::critical(m_game_list_frame, tr("Game Integrity"), text);
|
|
||||||
|
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);
|
}, 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);
|
0, 100, false, m_game_list_frame);
|
||||||
|
|
||||||
pdlg->setAutoClose(false);
|
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]()
|
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
|
// Set progress in range 0-100
|
||||||
const int progress = m_game_validator->get_size() ?
|
const int progress = m_game_validator->get_size() ?
|
||||||
@ -496,6 +545,7 @@ void game_list_actions::ShowGameIntegrityDialog(content_file_type file_type, con
|
|||||||
0;
|
0;
|
||||||
|
|
||||||
pdlg->setValue(progress);
|
pdlg->setValue(progress);
|
||||||
|
pdlg->setLabelText(tr("Calculating hash: %0").arg(m_game_validator->get_name()));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@ -608,19 +608,19 @@ void game_list_context_menu::show_single_selection_context_menu(const game_info&
|
|||||||
// Check disc game integrity
|
// Check disc game integrity
|
||||||
if (QString::fromStdString(current_game.category) == cat::cat_disc_game)
|
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);
|
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
|
// 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.
|
// 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
|
// 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"));
|
QAction* check_iso = check_integrity_menu->addAction(tr("&Check ISO Integrity"));
|
||||||
|
|
||||||
// If it's a Redump ISO and the integrity DB exists
|
// 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]()
|
connect(check_iso, &QAction::triggered, this, [this, gameinfo]()
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user