Add ISO integrity check

This commit is contained in:
Antonino Di Guardo 2026-04-22 15:06:02 +02:00 committed by GitHub
parent 80b6faef10
commit e26c80c129
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 748 additions and 79 deletions

View File

@ -10,6 +10,19 @@ rXmlNode::rXmlNode(const pugi::xml_node& node)
handle = node;
}
std::shared_ptr<rXmlNode> rXmlNode::GetChild(std::string_view name)
{
if (handle)
{
if (const pugi::xml_node child = handle.child(name))
{
return std::make_shared<rXmlNode>(child);
}
}
return nullptr;
}
std::shared_ptr<rXmlNode> rXmlNode::GetChildren()
{
if (handle)

View File

@ -20,6 +20,7 @@ struct rXmlNode
{
rXmlNode();
rXmlNode(const pugi::xml_node& node);
std::shared_ptr<rXmlNode> GetChild(std::string_view name);
std::shared_ptr<rXmlNode> GetChildren();
std::shared_ptr<rXmlNode> GetNext();
std::string GetName();

View File

@ -23,7 +23,24 @@
// Auxiliary functions (endian swap, xor).
// Hex string conversion auxiliary functions.
// Bytes conversion auxiliary function.
void bytes_to_hex(std::string& hex_str, const unsigned char* data, unsigned int data_length)
{
size_t str_length = data_length * 2;
hex_str.resize(str_length);
for (size_t i = 0; i < str_length; i += 2)
{
const auto [ptr, err] = std::to_chars(hex_str.data() + i, hex_str.data() + i + 2, *data++, 16);
if (err != std::errc())
{
fmt::throw_exception("Failed to read bytes: %s", std::make_error_code(err).message());
}
}
}
// Hex string conversion auxiliary function.
void hex_to_bytes(unsigned char* data, std::string_view hex_str, unsigned int str_length)
{
const auto strn_length = (str_length > 0) ? str_length : hex_str.size();

View File

@ -15,7 +15,10 @@ char* extract_file_name(const char* file_path, char real_file_name[CRYPTO_MAX_PA
std::string sha256_get_hash(const char* data, usz size, bool lower_case);
// Hex string conversion auxiliary functions.
// Bytes conversion auxiliary function.
void bytes_to_hex(std::string& hex_str, const unsigned char* data, unsigned int data_length);
// Hex string conversion auxiliary function.
void hex_to_bytes(unsigned char* data, std::string_view hex_str, unsigned int str_length);
// Crypto functions (AES128-CBC, AES128-ECB, SHA1-HMAC and AES-CMAC).

View File

@ -127,6 +127,7 @@ target_sources(rpcs3_emu PRIVATE
../Loader/TAR.cpp
../Loader/ISO.cpp
../Loader/iso_cache.cpp
../Loader/iso_validation.cpp
../Loader/TROPUSR.cpp
../Loader/TRP.cpp
)

View File

@ -241,6 +241,21 @@ namespace rpcs3::utils
return cache_dir;
}
std::string get_redump_db_path()
{
return fs::get_config_dir(true) + "redump.dat";
}
std::string get_redump_db_download_url()
{
return "https://api.rpcs3.net/redump/?api=v1";
}
std::string get_redump_key_dir()
{
return get_data_dir() + "redump/";
}
std::string get_data_dir()
{
return fs::get_config_dir() + "data/";

View File

@ -46,6 +46,10 @@ namespace rpcs3::utils
std::string get_cache_dir();
std::string get_cache_dir(std::string_view module_path);
std::string get_redump_db_path();
std::string get_redump_db_download_url();
std::string get_redump_key_dir();
std::string get_data_dir();
std::string get_icons_dir();
std::string get_savestates_dir();

View File

@ -2,6 +2,7 @@
#include "ISO.h"
#include "Emu/VFS.h"
#include "Emu/system_utils.hpp"
#include "Crypto/utils.h"
#include <codecvt>
@ -13,8 +14,6 @@
LOG_CHANNEL(sys_log, "SYS");
LOG_CHANNEL(iso_log, "ISO");
constexpr u64 ISO_SECTOR_SIZE = 2048;
struct iso_sector
{
u64 lba_address;
@ -28,25 +27,27 @@ struct iso_sector
bool is_file_iso(const std::string& path)
{
if (path.empty()) return false;
if (fs::is_dir(path)) return false;
if (path.empty() || fs::is_dir(path))
{
return false;
}
return is_file_iso(fs::file(path));
}
bool is_file_iso(const fs::file& file)
{
if (!file) return false;
if (file.size() < 32768 + 6) return false;
if (!file || file.size() < 32768 + 6)
{
return false;
}
file.seek(32768);
char magic[5];
file.read_at(32768 + 1, magic, 5);
return magic[0] == 'C' && magic[1] == 'D'
&& magic[2] == '0' && magic[3] == '0'
&& magic[4] == '1';
return magic[0] == 'C' && magic[1] == 'D' && magic[2] == '0' && magic[3] == '0' && magic[4] == '1';
}
// Convert 4 bytes in big-endian format to an unsigned integer
@ -79,7 +80,7 @@ static bool decrypt_data(aes_context& aes, u64 offset, unsigned char* buffer, u6
//if ((size % 16) != 0)
//{
// sys_log.error("decrypt_data(): Requested ciphertext blocks' size must be a multiple of 16 (%ull)", size);
// iso_log.error("decrypt_data: Requested ciphertext blocks' size must be a multiple of 16 (%ull)", size);
// return;
//}
@ -156,6 +157,83 @@ void iso_file_decryption::reset()
m_region_info.clear();
}
iso_type_status iso_file_decryption::check_type(const std::string& path, std::string& key_path, aes_context* aes_ctx)
{
if (!is_file_iso(path))
{
return iso_type_status::NOT_ISO;
}
// Remove file extension from file path
const usz ext_pos = path.rfind('.');
const std::string name_path = ext_pos == umax ? path : path.substr(0, ext_pos);
// Detect file name (with no parent folder and no file extension)
const usz name_pos = name_path.rfind('/');
const std::string name = name_pos == umax ? name_path : name_path.substr(name_pos);
fs::file key_file;
const std::array<std::string, 4> key_paths {
name_path + ".dkey",
name_path + ".key",
rpcs3::utils::get_redump_key_dir() + name + ".dkey",
rpcs3::utils::get_redump_key_dir() + name + ".key"
};
for (const std::string& path : key_paths)
{
key_file = fs::file(path);
if (key_file)
{
key_path = path;
break;
}
}
// If no ".dkey" and ".key" file exists
if (!key_file)
{
return iso_type_status::ERROR_OPENING_KEY;
}
char key_str[32];
std::array<u8, 16> key {};
const u64 key_len = key_file.read(key_str, sizeof(key_str));
if (key_len == sizeof(key_str) || key_len == sizeof(key))
{
// If the key read from the key file is 16 bytes long instead of 32, consider the file as
// binary (".key") and so not needing any further conversion from hex string to bytes
if (key_len == sizeof(key))
{
memcpy(key.data(), key_str, sizeof(key));
}
else
{
hex_to_bytes(key.data(), std::string_view(key_str, key_len), static_cast<unsigned int>(key_len));
}
aes_context aes_dec;
// If "aes_ctx" not requested
if (!aes_ctx)
{
aes_ctx = &aes_dec;
}
// Create the decryption context. If the context is successfully created, fill in "aes_ctx"
// (if requested) and return REDUMP_ISO
if (aes_setkey_dec(aes_ctx, key.data(), 128) == 0)
{
return iso_type_status::REDUMP_ISO;
}
}
return iso_type_status::ERROR_PROCESSING_KEY;
}
bool iso_file_decryption::init(const std::string& path)
{
reset();
@ -220,57 +298,25 @@ bool iso_file_decryption::init(const std::string& path)
// Check for Redump type
//
const usz ext_pos = path.rfind('.');
std::string key_path;
// If no file extension is provided, set "key_path" appending ".dkey" to "path".
// Otherwise, replace the extension (e.g. ".iso") with ".dkey"
key_path = ext_pos == umax ? path + ".dkey" : path.substr(0, ext_pos) + ".dkey";
fs::file key_file(key_path);
// If no ".dkey" file exists, try with ".key"
if (!key_file)
{
key_path = ext_pos == umax ? path + ".key" : path.substr(0, ext_pos) + ".key";
key_file = fs::file(key_path);
}
// Check if "key_path" exists and create the "m_aes_dec" context if so
if (key_file)
{
char key_str[32];
unsigned char key[16];
const u64 key_len = key_file.read(key_str, sizeof(key_str));
if (key_len == sizeof(key_str) || key_len == sizeof(key))
{
// If the key read from the key file is 16 bytes long instead of 32, consider the file as
// binary (".key") and so not needing any further conversion from hex string to bytes
if (key_len == sizeof(key))
{
memcpy(key, key_str, sizeof(key));
}
else
{
hex_to_bytes(key, std::string_view(key_str, key_len), static_cast<unsigned int>(key_len));
}
if (aes_setkey_dec(&m_aes_dec, key, 128) == 0)
{
m_enc_type = iso_encryption_type::REDUMP; // SET ENCRYPTION TYPE: REDUMP
}
}
if (m_enc_type == iso_encryption_type::NONE) // If encryption type was not set to REDUMP for any reason
{
iso_log.error("init: Failed to process key file: %s", key_path);
}
}
else
// Try to detect the Redump type. If so, the decryption context is set into "m_aes_dec"
switch (check_type(path, key_path, &m_aes_dec))
{
case iso_type_status::NOT_ISO:
iso_log.warning("init: Failed to recognize ISO file: %s", path);
break;
case iso_type_status::REDUMP_ISO:
m_enc_type = iso_encryption_type::REDUMP; // SET ENCRYPTION TYPE: REDUMP
break;
case iso_type_status::ERROR_OPENING_KEY:
iso_log.warning("init: Failed to open, or missing, key file: %s", key_path);
break;
case iso_type_status::ERROR_PROCESSING_KEY:
iso_log.error("init: Failed to process key file: %s", key_path);
break;
default:
break;
}
//
@ -402,12 +448,12 @@ inline T retrieve_endian_int(const u8* buf)
if constexpr (std::endian::little == std::endian::native)
{
// first half = little-endian copy
// First half = little-endian copy
std::memcpy(&out, buf, sizeof(T));
}
else
{
// second half = big-endian copy
// Second half = big-endian copy
std::memcpy(&out, buf + sizeof(T), sizeof(T));
}
@ -603,6 +649,7 @@ static void iso_form_hierarchy(fs::file& file, iso_fs_node& node, bool use_ucs2_
u64 iso_fs_metadata::size() const
{
u64 total_size = 0;
for (const auto& extent : extents)
{
total_size += extent.size;
@ -660,7 +707,10 @@ iso_archive::iso_archive(const std::string& path)
iso_fs_node* iso_archive::retrieve(const std::string& passed_path)
{
if (passed_path.empty()) return nullptr;
if (passed_path.empty())
{
return nullptr;
}
const std::string path = std::filesystem::path(passed_path).string();
const std::string_view path_sv = path;
@ -669,11 +719,16 @@ iso_fs_node* iso_archive::retrieve(const std::string& passed_path)
usz end = path_sv.find_first_of(fs::delim);
std::stack<iso_fs_node*> search_stack;
search_stack.push(&m_root);
do
{
if (search_stack.empty()) return nullptr;
if (search_stack.empty())
{
return nullptr;
}
const auto* top_entry = search_stack.top();
if (end == umax)
@ -681,7 +736,7 @@ iso_fs_node* iso_archive::retrieve(const std::string& passed_path)
end = path.size();
}
const std::string_view path_component = path_sv.substr(start, end-start);
const std::string_view path_component = path_sv.substr(start, end - start);
bool found = false;
@ -692,6 +747,7 @@ iso_fs_node* iso_archive::retrieve(const std::string& passed_path)
else if (path_component == "..")
{
search_stack.pop();
found = true;
}
else
@ -708,14 +764,20 @@ iso_fs_node* iso_archive::retrieve(const std::string& passed_path)
}
}
if (!found) return nullptr;
if (!found)
{
return nullptr;
}
start = end + 1;
end = path_sv.find_first_of(fs::delim, start);
}
while (start < path.size());
if (search_stack.empty()) return nullptr;
if (search_stack.empty())
{
return nullptr;
}
return search_stack.top();
}
@ -728,7 +790,11 @@ bool iso_archive::exists(const std::string& path)
bool iso_archive::is_file(const std::string& path)
{
const auto file_node = retrieve(path);
if (!file_node) return false;
if (!file_node)
{
return false;
}
return !file_node->metadata.is_directory;
}
@ -979,8 +1045,10 @@ u64 iso_file::seek(s64 offset, fs::seek_mode whence)
return -1;
}
const u64 result = m_file.seek(file_offset(m_pos));
if (result == umax) return umax;
if (m_file.seek(file_offset(new_pos)) == umax)
{
return umax;
}
m_pos = new_pos;
return m_pos;
@ -989,6 +1057,7 @@ u64 iso_file::seek(s64 offset, fs::seek_mode whence)
u64 iso_file::size()
{
u64 extent_sizes = 0;
for (const auto& extent : m_meta.extents)
{
extent_sizes += extent.size;
@ -1018,18 +1087,23 @@ bool iso_dir::read(fs::dir_entry& entry)
entry.size = selected.size();
m_pos++;
return true;
}
return false;
}
void iso_dir::rewind()
{
m_pos = 0;
}
bool iso_device::stat(const std::string& path, fs::stat_t& info)
{
const auto relative_path = std::filesystem::relative(std::filesystem::path(path), std::filesystem::path(fs_prefix)).string();
const auto node = m_archive.retrieve(relative_path);
if (!node)
{
fs::g_tls_error = fs::error::noent;
@ -1057,14 +1131,14 @@ bool iso_device::statfs(const std::string& path, fs::device_stat& info)
const auto relative_path = std::filesystem::relative(std::filesystem::path(path), std::filesystem::path(fs_prefix)).string();
const auto node = m_archive.retrieve(relative_path);
if (!node)
{
fs::g_tls_error = fs::error::noent;
return false;
}
const auto& meta = node->metadata;
const u64 size = meta.size();
const u64 size = node->metadata.size();
info = fs::device_stat
{
@ -1074,7 +1148,7 @@ bool iso_device::statfs(const std::string& path, fs::device_stat& info)
.avail_free = 0
};
return false;
return true;
}
std::unique_ptr<fs::file_base> iso_device::open(const std::string& path, bs_t<fs::open_mode> mode)
@ -1082,6 +1156,7 @@ std::unique_ptr<fs::file_base> iso_device::open(const std::string& path, bs_t<fs
const auto relative_path = std::filesystem::relative(std::filesystem::path(path), std::filesystem::path(fs_prefix)).string();
const auto node = m_archive.retrieve(relative_path);
if (!node)
{
fs::g_tls_error = fs::error::noent;
@ -1102,6 +1177,7 @@ std::unique_ptr<fs::dir_base> iso_device::open_dir(const std::string& path)
const auto relative_path = std::filesystem::relative(std::filesystem::path(path), std::filesystem::path(fs_prefix)).string();
const auto node = m_archive.retrieve(relative_path);
if (!node)
{
fs::g_tls_error = fs::error::noent;
@ -1118,11 +1194,6 @@ std::unique_ptr<fs::dir_base> iso_device::open_dir(const std::string& path)
return std::make_unique<iso_dir>(*node);
}
void iso_dir::rewind()
{
m_pos = 0;
}
void load_iso(const std::string& path)
{
sys_log.notice("Loading ISO '%s'", path);

View File

@ -1,6 +1,6 @@
#pragma once
#include "Loader/PSF.h"
#include "PSF.h"
#include "Utilities/File.h"
#include "util/types.hpp"
@ -12,6 +12,8 @@ bool is_file_iso(const fs::file& path);
void load_iso(const std::string& path);
void unload_iso();
constexpr u64 ISO_SECTOR_SIZE = 2048;
/*
- Hijacked the "iso_archive::iso_archive" method to test if the ".iso" file is encrypted and sets a flag.
The flag is set according to the first matching encryption type found following the order below:
@ -47,6 +49,15 @@ enum class iso_encryption_type
REDUMP
};
// Enum returned by checking type
enum class iso_type_status
{
NOT_ISO,
REDUMP_ISO,
ERROR_OPENING_KEY,
ERROR_PROCESSING_KEY
};
// ISO file decryption class
class iso_file_decryption
{
@ -58,6 +69,8 @@ private:
void reset();
public:
static iso_type_status check_type(const std::string& path, std::string& key_path, aes_context* aes_ctx = nullptr);
iso_encryption_type get_enc_type() const { return m_enc_type; }
bool init(const std::string& path);

View File

@ -0,0 +1,162 @@
#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& path, 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

@ -0,0 +1,42 @@
#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& path, 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

@ -203,6 +203,7 @@
</ClCompile>
<ClCompile Include="Emu\vfs_config.cpp" />
<ClCompile Include="Loader\disc.cpp" />
<ClCompile Include="Loader\iso_validation.cpp" />
<ClCompile Include="util\emu_utils.cpp" />
<ClCompile Include="util\serialization_ext.cpp">
<PrecompiledHeader>NotUsing</PrecompiledHeader>
@ -768,6 +769,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\mself.hpp" />
<ClInclude Include="util\atomic.hpp" />
<ClInclude Include="util\bless.hpp" />

View File

@ -1429,6 +1429,9 @@
<ClCompile Include="Emu\RSX\Overlays\overlay_select.cpp">
<Filter>Emu\GPU\RSX\Overlays</Filter>
</ClCompile>
<ClCompile Include="Loader\iso_validation.cpp">
<Filter>Loader</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="Crypto\aes.h">
@ -2875,6 +2878,9 @@
<ClInclude Include="Emu\RSX\Common\aligned_malloc.hpp">
<Filter>Emu\GPU\RSX\Common</Filter>
</ClInclude>
<ClInclude Include="Loader\iso_validation.h">
<Filter>Loader</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<None Include="Emu\RSX\Program\GLSLSnippets\GPUDeswizzle.glsl">

View File

@ -341,6 +341,9 @@
<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">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="QTGeneratedFiles\Debug\moc_kamen_rider_dialog.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</ClCompile>
@ -650,6 +653,9 @@
<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">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="QTGeneratedFiles\Release\moc_kamen_rider_dialog.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
</ClCompile>
@ -922,6 +928,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\progress_dialog.cpp" />
<ClCompile Include="rpcs3qt\qt_utils.cpp" />
<ClCompile Include="rpcs3qt\syntax_highlighter.cpp" />
@ -1915,6 +1922,16 @@
<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">
<AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(QTDIR)\bin\moc.exe;%(FullPath)</AdditionalInputs>
<Message Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Moc%27ing iso_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>
<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\memory_viewer_panel.h">
<AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(QTDIR)\bin\moc.exe;%(FullPath)</AdditionalInputs>
<Message Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Moc%27ing %(Identity)...</Message>

View File

@ -1302,6 +1302,15 @@
<ClCompile Include="rpcs3qt\emu_settings_type.cpp">
<Filter>Gui\settings</Filter>
</ClCompile>
<ClCompile Include="QTGeneratedFiles\Debug\moc_iso_integrity.cpp">
<Filter>Generated Files\Debug</Filter>
</ClCompile>
<ClCompile Include="QTGeneratedFiles\Release\moc_iso_integrity.cpp">
<Filter>Generated Files\Release</Filter>
</ClCompile>
<ClCompile Include="rpcs3qt\iso_integrity.cpp">
<Filter>Gui\game list</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="Input\ds4_pad_handler.h">
@ -2067,4 +2076,9 @@
<Filter>buildfiles\cmake</Filter>
</Text>
</ItemGroup>
<ItemGroup>
<CustomBuild Include="rpcs3qt\iso_integrity.h">
<Filter>Gui\game list</Filter>
</CustomBuild>
</ItemGroup>
</Project>

View File

@ -50,6 +50,7 @@ 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

@ -16,6 +16,8 @@
#include "Input/pad_thread.h"
#include <thread>
#include <QApplication>
#include <QCheckBox>
#include <QtConcurrent>
@ -365,6 +367,105 @@ 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)
{
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);
// 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]()
{
thread_base::set_name("Game Integrity");
QString text;
std::string hash, game_name;
bool info_dialog = false;
if (m_iso_validator->calculate_hash(hash) != iso_hash_status::COMPLETED)
{
text = "Hash calculation failed!\n\nIntegrity check aborted";
}
else
{
text = "Integrity check completed!\n\n";
switch (m_iso_validator->check_integrity(m_iso_validator->get_path(), hash, &game_name))
{
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;
}
}
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
{
sys_log.error("%s", text.toStdString());
QMessageBox::critical(m_game_list_frame, tr("Game Integrity"), text);
}
}, nullptr, false);
});
progress_dialog* pdlg = new progress_dialog(tr("ISO File Hash Calculation"), tr("Calculating hash"), tr("Cancel"),
0, 100, false, m_game_list_frame);
pdlg->setAutoClose(false);
pdlg->setAutoReset(false);
pdlg->open();
connect(pdlg, &progress_dialog::canceled, m_game_list_frame, [this]()
{
m_iso_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)
{
// 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 :
0;
pdlg->setValue(progress);
}
else
{
update_timer->stop();
update_timer->deleteLater();
// As last, close the progress bar (it will be already closed if the process was aborted) and delete the object
pdlg->close();
pdlg->deleteLater();
}
});
update_timer->start(500);
}
void game_list_actions::ShowDiskUsageDialog()
{
if (m_disk_usage_future.isRunning()) // Still running the last request

View File

@ -2,6 +2,7 @@
#include "gui_game_info.h"
#include "shortcut_utils.h"
#include "Loader/iso_validation.h"
#include <QFuture>
#include <QObject>
@ -54,6 +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 ShowDiskUsageDialog();
// NOTES:
@ -97,6 +99,8 @@ private:
game_list_frame* m_game_list_frame = nullptr;
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>();
// NOTE:
// m_content_info is used by:

View File

@ -16,6 +16,8 @@
#include "Utilities/File.h"
#include "Emu/system_utils.hpp"
#include "Loader/ISO.h"
#include "Loader/iso_validation.h"
#include "QApplication"
#include "QClipboard"
@ -600,6 +602,42 @@ void game_list_context_menu::show_single_selection_context_menu(const game_info&
addSeparator();
// Check integrity
if (QString::fromStdString(current_game.category) == cat::cat_disc_game)
{
std::string key_path;
const iso_type_status iso_type = iso_file_decryption::check_type(current_game.path, key_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)
{
const iso_integrity_status iso_integrity = iso_file_validation::check_integrity(current_game.path, "");
QAction* check_integrity = 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)
{
connect(check_integrity, &QAction::triggered, this, [this, gameinfo]()
{
m_game_list_actions->ShowGameIntegrityDialog(gameinfo);
});
}
else
{
check_integrity->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();
});
}
}
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"));

View File

@ -74,6 +74,7 @@ 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_game_compat = new game_compatibility(m_gui_settings, this);
m_config_db = new config_database(m_gui_settings, this);

View File

@ -3,6 +3,8 @@
#include "game_list.h"
#include "game_list_actions.h"
#include "custom_dock_widget.h"
#include "iso_integrity.h"
#include "Utilities/lockless.h"
#include "Utilities/mutex.h"
#include "util/auto_typemap.hpp"
@ -54,6 +56,7 @@ public:
void SetShowHidden(bool show);
iso_integrity* GetIsoIntegrity() const { return m_iso_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; }
@ -153,6 +156,7 @@ private:
// Game List
game_list_table* m_game_list = nullptr;
iso_integrity* m_iso_integrity = nullptr;
game_compatibility* m_game_compat = nullptr;
config_database* m_config_db = nullptr;
progress_dialog* m_progress_dialog = nullptr;

View File

@ -0,0 +1,113 @@
#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());
};

View File

@ -0,0 +1,26 @@
#pragma once
#include "downloader.h"
class iso_integrity : public QObject
{
Q_OBJECT
public:
// Handles download for the ISO integrity database
iso_integrity(QWidget* parent);
// Downloads and writes the database to file
void download();
private Q_SLOTS:
void handle_download_finished(const QByteArray& content);
void handle_download_canceled();
void handle_download_error(const QString& error);
private:
QByteArray read_json(const QByteArray& data, bool after_download);
QString m_filepath;
downloader* m_downloader = nullptr;
};