Cemu/src/gui/ChecksumTool.cpp
2023-09-08 02:09:03 +02:00

785 lines
23 KiB
C++

#include "gui/ChecksumTool.h"
#include "Cafe/TitleList/GameInfo.h"
#include "gui/helpers/wxCustomEvents.h"
#include "util/helpers/helpers.h"
#include "gui/helpers/wxHelpers.h"
#include "gui/wxHelper.h"
#include "Cafe/Filesystem/WUD/wud.h"
#include <zip.h>
#include <curl/curl.h>
#include <openssl/evp.h> /* EVP_Digest */
#include <openssl/sha.h> /* SHA256_DIGEST_LENGTH */
#include <rapidjson/document.h>
#include <rapidjson/istreamwrapper.h>
#include <rapidjson/ostreamwrapper.h>
#include <rapidjson/prettywriter.h>
#include <rapidjson/schema.h>
#include <wx/frame.h>
#include <wx/translation.h>
#include <wx/xrc/xmlres.h>
#include <wx/gauge.h>
#include <wx/gdicmn.h>
#include <wx/string.h>
#include <wx/sizer.h>
#include <wx/statbox.h>
#include <wx/stattext.h>
#include <wx/button.h>
#include <wx/filedlg.h>
#include <wx/dirdlg.h>
#include <wx/msgdlg.h>
#include "config/ActiveSettings.h"
#include "Cafe/TitleList/TitleList.h"
const char kSchema[] = R"(
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"properties": {
"title_id": {
"type": "string"
},
"region": {
"type": "integer"
},
"version": {
"type": "integer"
},
"wud_hash": {
"type": "string"
},
"files": {
"type": "array",
"items": {
"type": "object",
"properties": {
"file": {
"type": "string"
},
"hash": {
"type": "string"
}
},
"required": [
"file",
"hash"
]
}
}
},
"required": [
"title_id",
"region",
"version",
"files"
]
})";
ChecksumTool::ChecksumTool(wxWindow* parent, wxTitleManagerList::TitleEntry& entry)
: wxDialog(parent, wxID_ANY,
formatWxString(_("Title checksum of {:08x}-{:08x}"), (uint32) (entry.title_id >> 32), (uint32) (entry.title_id & 0xFFFFFFFF)),
wxDefaultPosition, wxDefaultSize, wxCAPTION | wxFRAME_TOOL_WINDOW | wxSYSTEM_MENU | wxTAB_TRAVERSAL | wxCLOSE_BOX), m_entry(entry)
{
m_info = CafeTitleList::GetTitleInfoByUID(m_entry.location_uid);
if (!m_info.IsValid())
throw std::runtime_error("Invalid title");
// only request online update once
static bool s_once = false;
if (!s_once)
{
s_once = true;
m_online_ready = std::async(std::launch::async, &ChecksumTool::LoadOnlineData, this);
}
else
m_enable_verify_button = 1;
auto* sizer = new wxBoxSizer(wxVERTICAL);
{
auto* box_sizer = new wxStaticBoxSizer(new wxStaticBox(this, wxID_ANY, _("Verifying integrity of game files...")), wxVERTICAL);
auto* box = box_sizer->GetStaticBox();
m_progress = new wxGauge(box, wxID_ANY, 100, wxDefaultPosition, wxDefaultSize, wxGA_HORIZONTAL | wxGA_SMOOTH);
m_progress->SetMinSize({ 400, -1 });
m_progress->SetValue(0);
box_sizer->Add(m_progress, 0, wxALL | wxEXPAND, 5);
m_status = new wxStaticText(box, wxID_ANY, wxEmptyString);
m_status->Wrap(-1);
box_sizer->Add(m_status, 0, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, 5);
sizer->Add(box_sizer, 0, wxEXPAND | wxALL, 5);
}
{
auto* box_sizer = new wxStaticBoxSizer(new wxStaticBox(this, wxID_ANY, _("Control")), wxHORIZONTAL);
auto* box = box_sizer->GetStaticBox();
m_verify_online = new wxButton(box, wxID_ANY, _("Verify online"));
m_verify_online->SetToolTip(_("Verifies the checksum online"));
m_verify_online->Disable();
m_verify_online->Bind(wxEVT_BUTTON, &ChecksumTool::OnVerifyOnline, this);
m_verify_online->Bind(wxEVT_ENABLE, [this](wxCommandEvent&)
{
++m_enable_verify_button;
if (m_enable_verify_button >= 2)
{
// only enable if we have a file for it
const auto title_id_str = fmt::format("{:016x}", m_json_entry.title_id);
const auto default_file = fmt::format("{}_v{}.json", title_id_str, m_info.GetAppTitleVersion());
const auto checksum_path = ActiveSettings::GetUserDataPath("resources/checksums/{}", default_file);
if (exists(checksum_path))
m_verify_online->Enable();
}
});
box_sizer->Add(m_verify_online, 0, wxALL | wxEXPAND, 5);
m_verify_local = new wxButton(box, wxID_ANY, _("Verify with local file"));
m_verify_online->SetToolTip(_("Verifies the checksum with a local JSON file you can select"));
m_verify_local->Disable();
m_verify_local->Bind(wxEVT_BUTTON, &ChecksumTool::OnVerifyLocal, this);
box_sizer->Add(m_verify_local, 0, wxALL | wxEXPAND, 5);
m_export_button = new wxButton(box, wxID_ANY, _("Export"));
m_verify_online->SetToolTip(_("Export the title checksum data to a local JSON file"));
m_export_button->Disable();
m_export_button->Bind(wxEVT_BUTTON, &ChecksumTool::OnExportChecksums, this);
box_sizer->Add(m_export_button, 0, wxALL | wxEXPAND, 5);
sizer->Add(box_sizer, 0, wxEXPAND | wxALL, 5);
}
this->Bind(wxEVT_SET_GAUGE_VALUE, &ChecksumTool::OnSetGaugevalue, this);
m_worker = std::thread(&ChecksumTool::DoWork, this);
this->SetSizerAndFit(sizer);
this->Centre(wxBOTH);
}
ChecksumTool::~ChecksumTool()
{
m_running = false;
if (m_worker.joinable())
m_worker.join();
}
std::size_t WriteCallback(const char* in, std::size_t size, std::size_t num, std::string* out)
{
const std::size_t totalBytes(size * num);
out->append(in, totalBytes);
return totalBytes;
}
void ChecksumTool::LoadOnlineData() const
{
try
{
bool updated_required = true;
std::string latest_commit;
const auto checksum_path = ActiveSettings::GetUserDataPath("resources/checksums");
if (exists(checksum_path))
{
std::string current_commit;
// check for current version
std::ifstream file(checksum_path / "commit.txt");
if (file.is_open())
{
std::getline(file, current_commit);
file.close();
}
// check latest version
/*
https://api.github.com/repos/teamcemu/title-checksums/branches/master
https://api.github.com/repos/teamcemu/title-checksums/commits?per_page=1
*/
std::string data;
auto* curl = curl_easy_init();
curl_easy_setopt(curl, CURLOPT_URL, "https://api.github.com/repos/teamcemu/title-checksums/commits/master");
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &data);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
curl_easy_setopt(curl, CURLOPT_USERAGENT, BUILD_VERSION_WITH_NAME_STRING);
curl_easy_perform(curl);
long http_code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
curl_easy_cleanup(curl);
if (http_code == 200 && !data.empty())
{
rapidjson::Document doc;
doc.Parse(data.c_str(), data.size());
if (!doc.HasParseError() && doc.HasMember("sha"))
{
latest_commit = doc["sha"].GetString();
if (boost::iequals(current_commit, latest_commit))
{
updated_required = false;
}
}
}
}
else
{
// create directory since not available yet
fs::create_directories(checksum_path);
}
if (updated_required)
{
std::string data;
auto* curl = curl_easy_init();
curl_easy_setopt(curl, CURLOPT_URL, "https://github.com/TeamCemu/title-checksums/archive/master.zip");
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &data);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1);
curl_easy_setopt(curl, CURLOPT_USERAGENT, BUILD_VERSION_WITH_NAME_STRING);
curl_easy_perform(curl);
long http_code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
curl_easy_cleanup(curl);
if (http_code == 200 && !data.empty())
{
// init zip source
zip_error_t error;
zip_error_init(&error);
zip_source_t* src;
if ((src = zip_source_buffer_create(data.data(), data.size(), 1, &error)) == nullptr)
{
zip_error_fini(&error);
return;
}
auto* za = zip_open_from_source(src, ZIP_RDONLY, &error);
if (!za)
{
wxQueueEvent(m_verify_online, new wxCommandEvent(wxEVT_ENABLE));
return;
}
const auto numEntries = zip_get_num_entries(za, 0);
for (sint64 i = 0; i < numEntries; i++)
{
zip_stat_t sb = { 0 };
if (zip_stat_index(za, i, 0, &sb) != 0)
continue;
if (std::strstr(sb.name, "../") != nullptr ||
std::strstr(sb.name, "..\\") != nullptr)
continue; // bad path
if (boost::equals(sb.name, "title-checksums-master/"))
continue;
// title-checksums-master/
const auto path = checksum_path / &sb.name[sizeof("title-checksums-master")];
size_t sbNameLen = strlen(sb.name);
if (sbNameLen == 0)
continue;
if (sb.name[sbNameLen - 1] == '/')
{
std::error_code ec;
fs::create_directories(path, ec);
continue;
}
if (sb.size == 0)
continue;
if (sb.size > (1024 * 1024 * 128))
continue; // skip unusually huge files
zip_file_t* zipFile = zip_fopen_index(za, i, 0);
if (zipFile == nullptr)
continue;
std::vector<char> buffer(sb.size);
if (zip_fread(zipFile, buffer.data(), sb.size) == sb.size)
{
std::ofstream file(path);
if (file.is_open())
{
file.write(buffer.data(), sb.size);
file.flush();
file.close();
}
}
zip_fclose(zipFile);
}
std::ofstream file(checksum_path / "commit.txt");
if (file.is_open())
{
file << latest_commit;
file.close();
}
}
}
}
catch(const std::exception& ex)
{
cemuLog_log(LogType::Force, "error on updating json checksum data: {}", ex.what());
}
wxQueueEvent(m_verify_online, new wxCommandEvent(wxEVT_ENABLE));
}
void ChecksumTool::OnSetGaugevalue(wxSetGaugeValue& event)
{
event.GetGauge()->SetValue(event.GetValue());
event.GetTextCtrl()->SetLabelText(event.GetText());
// no error
if(event.GetInt() == 0 && event.GetValue() == 100)
{
m_export_button->Enable();
m_verify_local->Enable();
wxPostEvent(m_verify_online, wxCommandEvent(wxEVT_ENABLE));
}
}
void ChecksumTool::OnExportChecksums(wxCommandEvent& event)
{
// TODO: merge if json already exists
wxDirDialog dialog(this, _("Export checksum entry"), "", wxDD_DEFAULT_STYLE | wxDD_DIR_MUST_EXIST);
if (dialog.ShowModal() != wxID_OK || dialog.GetPath().IsEmpty())
return;
rapidjson::Document doc;
doc.SetObject();
auto& a = doc.GetAllocator();
/*
title_id
region
version
wud_hash
files:
[
{file, hash}
]
*/
auto title_id_str = fmt::format("{:016x}", m_json_entry.title_id);
doc.AddMember("title_id", rapidjson::StringRef(title_id_str.c_str(), title_id_str.size()), a);
doc.AddMember("region", (int)m_info.GetMetaRegion(), a);
doc.AddMember("version", m_info.GetAppTitleVersion(), a);
if (!m_json_entry.wud_hash.empty())
doc.AddMember("wud_hash", rapidjson::StringRef(m_json_entry.wud_hash.c_str(), m_json_entry.wud_hash.size()), a);
rapidjson::Value entry_array(rapidjson::kArrayType);
rapidjson::Value file_array(rapidjson::kArrayType);
for(const auto& file : m_json_entry.file_hashes)
{
rapidjson::Value file_entry;
file_entry.SetObject();
file_entry.AddMember("file", rapidjson::StringRef(file.first.c_str(), file.first.size()), a);
file_entry.AddMember("hash", rapidjson::StringRef(file.second.c_str(), file.second.size()), a);
file_array.PushBack(file_entry, a);
}
doc.AddMember("files", file_array, a);
std::filesystem::path target_file{ dialog.GetPath().c_str().AsInternal() };
target_file /= fmt::format("{}_v{}.json", title_id_str, m_info.GetAppTitleVersion());
std::ofstream file(target_file);
if(file.is_open())
{
rapidjson::OStreamWrapper osw(file);
rapidjson::PrettyWriter<rapidjson::OStreamWrapper> writer(osw);
//rapidjson::GenericSchemaValidator<rapidjson::SchemaDocument, rapidjson::Writer<rapidjson::StringBuffer> > validator(schema, writer);
doc.Accept(writer);
wxMessageBox(_("Export successful"), wxMessageBoxCaptionStr, wxOK | wxCENTRE, this);
}
else
{
wxMessageBox(formatWxString(_("Can't write to file: {}"), target_file.string()), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this);
}
}
void ChecksumTool::VerifyJsonEntry(const rapidjson::Document& doc)
{
rapidjson::Document sdoc;
sdoc.Parse(kSchema, std::size(kSchema));
wxASSERT(!sdoc.HasParseError());
rapidjson::SchemaDocument schema(sdoc);
rapidjson::SchemaValidator validator(schema);
if (!doc.Accept(validator))
{
//// validation error:
//rapidjson::StringBuffer sb;
//validator.GetInvalidSchemaPointer().StringifyUriFragment(sb);
//printf("Invalid schema: %s\n", sb.GetString());
//printf("Invalid keyword: %s\n", validator.GetInvalidSchemaKeyword());
//sb.Clear();
//validator.GetInvalidDocumentPointer().StringifyUriFragment(sb);
//printf("Invalid document: %s\n", sb.GetString());
///*
//Invalid schema: #
//Invalid keyword: required
//Invalid document: #
// */
wxMessageBox(_("JSON file doesn't satisfy needed schema"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this);
return;
}
try
{
JsonEntry test_entry{};
test_entry.title_id = ConvertString<uint64>(doc["title_id"].GetString(), 16);
test_entry.region = (CafeConsoleRegion)doc["region"].GetInt();
test_entry.version = doc["version"].GetInt();
if (doc.HasMember("wud_hash"))
test_entry.wud_hash = doc["wud_hash"].GetString();
for (const auto& v : doc["files"].GetArray())
{
std::filesystem::path genericFilePath(v["file"].GetString(), std::filesystem::path::generic_format); // convert path to generic form (forward slashes)
test_entry.file_hashes[genericFilePath.generic_string()] = v["hash"].GetString();
}
if (m_json_entry.title_id != test_entry.title_id)
{
wxMessageBox(formatWxString(_("The file you are comparing with is for a different title.")), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this);
return;
}
if (m_json_entry.version != test_entry.version)
{
wxMessageBox(formatWxString(_("Wrong version: {}"), test_entry.version), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this);
return;
}
if (m_json_entry.region != test_entry.region)
{
wxMessageBox(formatWxString(_("Wrong region: {}"), test_entry.region), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this);
return;
}
if (!m_json_entry.wud_hash.empty())
{
if (test_entry.wud_hash.empty())
{
wxMessageBox(_("The verification data doesn't include a WUD hash!"), _("Error"), wxOK | wxCENTRE | wxICON_WARNING, this);
return;
}
if(!boost::iequals(test_entry.wud_hash, m_json_entry.wud_hash))
{
wxMessageBox(formatWxString(_("Your game image is invalid!\n\nYour hash:\n{}\n\nExpected hash:\n{}"), m_json_entry.wud_hash, test_entry.wud_hash), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this);
return;
}
}
else
{
std::map<std::string_view, std::pair<std::string, std::string>> invalid_hashes;
std::vector<std::string_view> missing_files;
const auto writeMismatchInfoToLog = [this, &missing_files, &invalid_hashes]()
{
wxFileDialog dialog(this, _("Select a file to export the errors"), wxEmptyString, wxEmptyString, "Error list (*.txt)|*.txt", wxFD_SAVE | wxFD_OVERWRITE_PROMPT);
if (dialog.ShowModal() != wxID_OK || dialog.GetPath().IsEmpty())
return;
const std::string path = dialog.GetPath().ToUTF8().data();
std::ofstream file(path);
if (file.is_open())
{
if (!missing_files.empty())
{
file << "The following files are missing:\n";
for (const auto& f : missing_files)
file << "\t" << f << "\n";
file << "\n";
}
if (!invalid_hashes.empty())
{
file << "The following files have an invalid hash (name | current hash | expected hash):\n";
for (const auto& f : invalid_hashes)
file << "\t" << f.first << " | " << f.second.first << " | " << f.second.second << "\n";
}
file.flush();
file.close();
wxLaunchDefaultBrowser(wxHelper::FromUtf8(fmt::format("file:{}", path)));
}
else
wxMessageBox(_("Can't open file to write!"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this);
};
for (const auto& f : test_entry.file_hashes)
{
const auto it = m_json_entry.file_hashes.find(f.first);
if (it == m_json_entry.file_hashes.cend())
{
missing_files.emplace_back(f.first);
}
else if (!boost::iequals(f.second, it->second))
{
invalid_hashes[f.first] = std::make_pair(it->second, f.second);
}
}
// files are missing but rest is okay
if ((!missing_files.empty() || !invalid_hashes.empty()) && (missing_files.size() + invalid_hashes.size()) < 30)
{
// the list of missing + invalid hashes is short enough that we can print it to the message box
std::stringstream str;
if (missing_files.size() > 0)
{
str << _("The following files are missing:").ToUTF8().data() << "\n";
for (const auto& v : missing_files)
str << v << "\n";
if(invalid_hashes.size() > 0)
str << "\n";
}
if (invalid_hashes.size() > 0)
{
str << _("The following files are damaged:").ToUTF8().data() << "\n";
for (const auto& v : invalid_hashes)
str << v.first << "\n";
}
wxMessageBox(str.str(), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this);
return;
}
else if (missing_files.empty() && !invalid_hashes.empty())
{
const int result = wxMessageBox(formatWxString(
_("{} files have an invalid hash!\nDo you want to export a list of them to a file?"),
invalid_hashes.size()), _("Error"), wxYES_NO | wxCENTRE | wxICON_ERROR, this);
if (result == wxYES)
{
writeMismatchInfoToLog();
}
return;
}
else if (!missing_files.empty() && !invalid_hashes.empty())
{
const int result = wxMessageBox(formatWxString(
_("Multiple issues with your game files have been found!\nDo you want to export them to a file?"),
invalid_hashes.size()), _("Error"), wxYES_NO | wxCENTRE | wxICON_ERROR, this);
if (result == wxYES)
{
writeMismatchInfoToLog();
}
return;
}
}
wxMessageBox(_("Your game files are valid"), _("Success"), wxOK | wxCENTRE, this);
}
catch (const std::exception& ex)
{
wxMessageBox(formatWxString(_("JSON parse error: {}"), ex.what()), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this);
}
}
void ChecksumTool::OnVerifyOnline(wxCommandEvent& event)
{
const auto title_id_str = fmt::format("{:016x}", m_json_entry.title_id);
const auto default_file = fmt::format("{}_v{}.json", title_id_str, m_info.GetAppTitleVersion());
const auto checksum_path = ActiveSettings::GetUserDataPath("resources/checksums/{}", default_file);
if(!exists(checksum_path))
return;
std::ifstream file(checksum_path);
if (!file.is_open())
{
wxMessageBox(_("Can't open file!"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this);
return;
}
rapidjson::IStreamWrapper str(file);
rapidjson::Document d;
d.ParseStream(str);
if (d.HasParseError())
{
wxMessageBox(_("Can't parse JSON file!"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this);
return;
}
VerifyJsonEntry(d);
}
void ChecksumTool::OnVerifyLocal(wxCommandEvent& event)
{
const auto title_id_str = fmt::format("{:016x}", m_json_entry.title_id);
const auto default_file = fmt::format("{}_v{}.json", title_id_str, m_info.GetAppTitleVersion());
wxFileDialog file_dialog(this, _("Open checksum entry"), "", default_file.c_str(),"JSON files (*.json)|*.json", wxFD_OPEN | wxFD_FILE_MUST_EXIST);
if (file_dialog.ShowModal() != wxID_OK || file_dialog.GetPath().IsEmpty())
return;
std::filesystem::path filename{ file_dialog.GetPath().c_str().AsInternal() };
std::ifstream file(filename);
if(!file.is_open())
{
wxMessageBox(_("Can't open file!"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this);
return;
}
rapidjson::IStreamWrapper str(file);
rapidjson::Document d;
d.ParseStream(str);
if (d.HasParseError())
{
wxMessageBox(_("Can't parse JSON file!"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this);
return;
}
VerifyJsonEntry(d);
}
static void _fscGetAllFiles(std::set<std::string>& allFilesOut, const std::string& fscBasePath, const std::string& relativePath)
{
sint32 fscStatus;
FSCVirtualFile* fsc = fsc_openDirIterator((fscBasePath + relativePath).c_str(), &fscStatus);
cemu_assert(fsc);
FSCDirEntry dirEntry;
while (fsc_nextDir(fsc, &dirEntry))
{
if (dirEntry.isDirectory)
{
_fscGetAllFiles(allFilesOut, fscBasePath, std::string(relativePath).append(dirEntry.GetPath()).append("/"));
}
else
{
allFilesOut.emplace(std::string(relativePath).append(dirEntry.GetPath()));
}
}
delete fsc;
}
void ChecksumTool::DoWork()
{
m_json_entry.title_id = m_info.GetAppTitleId();
m_json_entry.region = m_info.GetMetaRegion();
m_json_entry.version = m_info.GetAppTitleVersion();
static_assert(SHA256_DIGEST_LENGTH == 32);
std::array<uint8, SHA256_DIGEST_LENGTH> checksum{};
switch (m_info.GetFormat())
{
case TitleInfo::TitleDataFormat::WUD:
{
const auto path = m_entry.path.string();
wxQueueEvent(this, new wxSetGaugeValue(1, m_progress, m_status, formatWxString(_("Reading game image: {}"), path)));
wud_t* wud = wud_open(m_info.GetPath());
if (!wud)
throw std::runtime_error("can't open game image");
const auto wud_size = wud_getWUDSize(wud);
std::vector<uint8> buffer(1024 * 1024 * 8);
EVP_MD_CTX *sha256 = EVP_MD_CTX_new();
EVP_DigestInit(sha256, EVP_sha256());
uint32 read = 0;
size_t offset = 0;
auto size = wud_size;
do
{
if (!m_running.load(std::memory_order_relaxed))
{
wud_close(wud);
return;
}
read = wud_readData(wud, buffer.data(), std::min(buffer.size(), (size_t)wud_size - offset), offset);
offset += read;
size -= read;
EVP_DigestUpdate(sha256, buffer.data(), read);
wxQueueEvent(this, new wxSetGaugeValue((int)((offset * 90) / wud_size), m_progress, m_status, formatWxString(_("Reading game image: {0}/{1} kB"), offset / 1024, wud_size / 1024)));
} while (read != 0 && size > 0);
wud_close(wud);
wxQueueEvent(this, new wxSetGaugeValue(90, m_progress, m_status, formatWxString(_("Generating checksum of game image: {}"), path)));
if (!m_running.load(std::memory_order_relaxed))
return;
EVP_DigestFinal_ex(sha256, checksum.data(), NULL);
EVP_MD_CTX_free(sha256);
std::stringstream str;
for (const auto& b : checksum)
{
str << fmt::format("{:02X}", b);
}
m_json_entry.wud_hash = str.str();
wxQueueEvent(this, new wxSetGaugeValue(100, m_progress, m_status, formatWxString(_("Generated checksum of game image: {}"), path)));
break;
}
default:
// we hash the individual files for all formats except WUD/WUX
std::string temporaryMountPath = TitleInfo::GetUniqueTempMountingPath();
m_info.Mount(temporaryMountPath.c_str(), "", FSC_PRIORITY_BASE);
wxQueueEvent(this, new wxSetGaugeValue(1, m_progress, m_status, _("Grabbing game files")));
// get list of all files
std::set<std::string> files;
_fscGetAllFiles(files, temporaryMountPath, "");
const size_t file_count = files.size();
size_t counter = 0;
for (const auto& filename : files)
{
auto fileData = fsc_extractFile((temporaryMountPath + "/" + filename).c_str());
if (!fileData)
{
cemuLog_log(LogType::Force, "Failed to open {}", filename);
continue;
}
SHA256(fileData->data(), fileData->size(), checksum.data());
std::stringstream str;
for (const auto& b : checksum)
{
str << fmt::format("{:02X}", b);
}
// store relative path and hash
m_json_entry.file_hashes[filename] = str.str();
++counter;
wxQueueEvent(this, new wxSetGaugeValue((int)((counter * 100) / file_count), m_progress, m_status, formatWxString(_("Hashing game file: {}/{}"), counter, file_count)));
if (!m_running.load(std::memory_order_relaxed))
{
m_info.Unmount(temporaryMountPath.c_str());
return;
}
}
m_info.Unmount(temporaryMountPath.c_str());
wxQueueEvent(this, new wxSetGaugeValue(100, m_progress, m_status, formatWxString(_("Generated checksum of {} game files"), file_count)));
break;
}
}