This commit is contained in:
Antonino Di Guardo 2026-05-12 22:54:21 +05:30 committed by GitHub
commit 8f119ef0f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 647 additions and 402 deletions

View File

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

View File

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

View File

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

View File

@ -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<rXmlNode> 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<u8, block_size> 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;
}

View File

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

View File

@ -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<rXmlNode> 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<u8, block_size> 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;
}

View File

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

View File

@ -205,7 +205,7 @@
</ClCompile>
<ClCompile Include="Emu\vfs_config.cpp" />
<ClCompile Include="Loader\disc.cpp" />
<ClCompile Include="Loader\iso_validation.cpp" />
<ClCompile Include="Loader\content_validation.cpp" />
<ClCompile Include="util\emu_utils.cpp" />
<ClCompile Include="util\serialization_ext.cpp">
<PrecompiledHeader>NotUsing</PrecompiledHeader>
@ -780,7 +780,7 @@
<ClInclude Include="Emu\system_config_types.h" />
<ClInclude Include="Emu\vfs_config.h" />
<ClInclude Include="Loader\disc.h" />
<ClInclude Include="Loader\iso_validation.h" />
<ClInclude Include="Loader\content_validation.h" />
<ClInclude Include="Loader\mself.hpp" />
<ClInclude Include="util\atomic.hpp" />
<ClInclude Include="util\bless.hpp" />
@ -1063,6 +1063,7 @@
<ClInclude Include="Loader\PUP.h" />
<ClInclude Include="Loader\TAR.h" />
<ClInclude Include="Loader\ISO.h" />
<ClInclude Include="Loader\iso_cache.h" />
<ClInclude Include="Loader\TROPUSR.h" />
<ClInclude Include="Loader\TRP.h" />
<ClInclude Include="rpcs3_version.h" />

View File

@ -1414,6 +1414,9 @@
<ClCompile Include="Loader\ISO.cpp">
<Filter>Loader</Filter>
</ClCompile>
<ClCompile Include="Loader\iso_cache.cpp">
<Filter>Loader</Filter>
</ClCompile>
<ClCompile Include="Emu\RSX\Overlays\overlay_audio.cpp">
<Filter>Emu\GPU\RSX\Overlays</Filter>
</ClCompile>
@ -1432,7 +1435,7 @@
<ClCompile Include="Emu\RSX\Overlays\overlay_select.cpp">
<Filter>Emu\GPU\RSX\Overlays</Filter>
</ClCompile>
<ClCompile Include="Loader\iso_validation.cpp">
<ClCompile Include="Loader\content_validation.cpp">
<Filter>Loader</Filter>
</ClCompile>
<ClCompile Include="..\Utilities\stereo_config.cpp">
@ -2857,6 +2860,9 @@
<ClInclude Include="Loader\ISO.h">
<Filter>Loader</Filter>
</ClInclude>
<ClInclude Include="Loader\iso_cache.h">
<Filter>Loader</Filter>
</ClInclude>
<ClInclude Include="Emu\RSX\Overlays\overlay_audio.h">
<Filter>Emu\GPU\RSX\Overlays</Filter>
</ClInclude>
@ -2881,7 +2887,7 @@
<ClInclude Include="Emu\RSX\Common\aligned_malloc.hpp">
<Filter>Emu\GPU\RSX\Common</Filter>
</ClInclude>
<ClInclude Include="Loader\iso_validation.h">
<ClInclude Include="Loader\content_validation.h">
<Filter>Loader</Filter>
</ClInclude>
<ClInclude Include="..\Utilities\stereo_config.h">

View File

@ -341,7 +341,7 @@
<ClCompile Include="QTGeneratedFiles\Debug\moc_ipc_settings_dialog.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="QTGeneratedFiles\Debug\moc_iso_integrity.cpp">
<ClCompile Include="QTGeneratedFiles\Debug\moc_content_integrity.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="QTGeneratedFiles\Debug\moc_kamen_rider_dialog.cpp">
@ -653,7 +653,7 @@
<ClCompile Include="QTGeneratedFiles\Release\moc_ipc_settings_dialog.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="QTGeneratedFiles\Release\moc_iso_integrity.cpp">
<ClCompile Include="QTGeneratedFiles\Release\moc_content_integrity.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="QTGeneratedFiles\Release\moc_kamen_rider_dialog.cpp">
@ -929,7 +929,7 @@
<ClCompile Include="rpcs3qt\config_database.cpp" />
<ClCompile Include="rpcs3qt\game_compatibility.cpp" />
<ClCompile Include="rpcs3qt\game_list_grid.cpp" />
<ClCompile Include="rpcs3qt\iso_integrity.cpp" />
<ClCompile Include="rpcs3qt\content_integrity.cpp" />
<ClCompile Include="rpcs3qt\progress_dialog.cpp" />
<ClCompile Include="rpcs3qt\qt_utils.cpp" />
<ClCompile Include="rpcs3qt\syntax_highlighter.cpp" />
@ -1924,13 +1924,13 @@
<Outputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp</Outputs>
<Command Condition="'$(Configuration)|$(Platform)'=='Release|x64'">"$(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"</Command>
</CustomBuild>
<CustomBuild Include="rpcs3qt\iso_integrity.h">
<CustomBuild Include="rpcs3qt\content_integrity.h">
<AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(QTDIR)\bin\moc.exe;%(FullPath)</AdditionalInputs>
<Message Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Moc%27ing iso_integrity.h...</Message>
<Message Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Moc%27ing content_integrity.h...</Message>
<Outputs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">.\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp</Outputs>
<Command Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">"$(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"</Command>
<AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(QTDIR)\bin\moc.exe;%(FullPath)</AdditionalInputs>
<Message Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Moc%27ing iso_integrity.h...</Message>
<Message Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Moc%27ing content_integrity.h...</Message>
<Outputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp</Outputs>
<Command Condition="'$(Configuration)|$(Platform)'=='Release|x64'">"$(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"</Command>
</CustomBuild>

View File

@ -1302,13 +1302,13 @@
<ClCompile Include="rpcs3qt\emu_settings_type.cpp">
<Filter>Gui\settings</Filter>
</ClCompile>
<ClCompile Include="QTGeneratedFiles\Debug\moc_iso_integrity.cpp">
<ClCompile Include="QTGeneratedFiles\Debug\moc_content_integrity.cpp">
<Filter>Generated Files\Debug</Filter>
</ClCompile>
<ClCompile Include="QTGeneratedFiles\Release\moc_iso_integrity.cpp">
<ClCompile Include="QTGeneratedFiles\Release\moc_content_integrity.cpp">
<Filter>Generated Files\Release</Filter>
</ClCompile>
<ClCompile Include="rpcs3qt\iso_integrity.cpp">
<ClCompile Include="rpcs3qt\content_integrity.cpp">
<Filter>Gui\game list</Filter>
</ClCompile>
<ClCompile Include="rpcs3qt\anaglyph_settings_dialog.cpp">
@ -2083,7 +2083,7 @@
</Text>
</ItemGroup>
<ItemGroup>
<CustomBuild Include="rpcs3qt\iso_integrity.h">
<CustomBuild Include="rpcs3qt\content_integrity.h">
<Filter>Gui\game list</Filter>
</CustomBuild>
</ItemGroup>

View File

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

View File

@ -0,0 +1,134 @@
#include "content_integrity.h"
#include "gui_settings.h"
#include "Emu/system_utils.hpp"
#include <QJsonDocument>
#include <QJsonObject>
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());
};

View File

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

View File

@ -23,6 +23,7 @@
#include <QtConcurrent>
#include <QDir>
#include <QDirIterator>
#include <QFileDialog>
#include <QGridLayout>
#include <QMessageBox>
#include <QTimer>
@ -367,67 +368,167 @@ void game_list_actions::ShowGameInfoDialog(const std::vector<game_info>& 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<float>(m_iso_validator->get_bytes_read()) / m_iso_validator->get_size()) * 100 :
const int progress = m_game_validator->get_size() ?
(static_cast<float>(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
{

View File

@ -2,7 +2,7 @@
#include "gui_game_info.h"
#include "shortcut_utils.h"
#include "Loader/iso_validation.h"
#include "Loader/content_validation.h"
#include <QFuture>
#include <QObject>
@ -55,7 +55,7 @@ public:
void ShowRemoveGameDialog(const std::vector<game_info>& games);
void ShowGameInfoDialog(const std::vector<game_info>& 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<gui_settings> m_gui_settings;
QFuture<void> m_disk_usage_future;
QFuture<void> m_game_integrity_future;
std::shared_ptr<iso_file_validation> m_iso_validator = std::make_shared<iso_file_validation>();
std::shared_ptr<content_validation> m_game_validator = std::make_shared<content_validation>();
// NOTE:
// m_content_info is used by:

View File

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

View File

@ -74,7 +74,10 @@ game_list_frame::game_list_frame(std::shared_ptr<gui_settings> 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);

View File

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

View File

@ -1,113 +0,0 @@
#include "iso_integrity.h"
#include "gui_settings.h"
#include "Emu/system_utils.hpp"
#include <QJsonDocument>
#include <QJsonObject>
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());
};