mirror of
https://github.com/RPCS3/rpcs3.git
synced 2026-04-29 23:41:12 -06:00
Add ISO integrity check
This commit is contained in:
parent
80b6faef10
commit
e26c80c129
@ -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)
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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).
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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/";
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
162
rpcs3/Loader/iso_validation.cpp
Normal file
162
rpcs3/Loader/iso_validation.cpp
Normal 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;
|
||||
}
|
||||
42
rpcs3/Loader/iso_validation.h
Normal file
42
rpcs3/Loader/iso_validation.h
Normal 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);
|
||||
};
|
||||
@ -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" />
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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"));
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
113
rpcs3/rpcs3qt/iso_integrity.cpp
Normal file
113
rpcs3/rpcs3qt/iso_integrity.cpp
Normal 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());
|
||||
};
|
||||
26
rpcs3/rpcs3qt/iso_integrity.h
Normal file
26
rpcs3/rpcs3qt/iso_integrity.h
Normal 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;
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user