From 24bdf0a156b2e51050635e2f3fd8a8ff6f1c67a4 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sat, 2 May 2020 00:06:46 +0800 Subject: [PATCH] core, frontend: Add 'Dump CXI file' option Right click on an application in the Select Contents dialog. --- src/core/importer.cpp | 50 ++++++++++-- src/core/importer.h | 41 +++++++--- src/core/ncch/ncch_container.cpp | 22 +++++- src/core/ncch/ncch_container.h | 12 ++- src/frontend/CMakeLists.txt | 6 +- src/frontend/{ => helpers}/import_job.cpp | 6 +- src/frontend/{ => helpers}/import_job.h | 0 src/frontend/helpers/progressive_job.cpp | 27 +++++++ src/frontend/helpers/progressive_job.h | 36 +++++++++ src/frontend/import_dialog.cpp | 96 ++++++++++++++++++++++- src/frontend/import_dialog.h | 7 ++ src/frontend/import_dialog.ui | 3 + 12 files changed, 275 insertions(+), 31 deletions(-) rename src/frontend/{ => helpers}/import_job.cpp (93%) rename src/frontend/{ => helpers}/import_job.h (100%) create mode 100644 src/frontend/helpers/progressive_job.cpp create mode 100644 src/frontend/helpers/progressive_job.h diff --git a/src/core/importer.cpp b/src/core/importer.cpp index ef8f73d..94165e7 100644 --- a/src/core/importer.cpp +++ b/src/core/importer.cpp @@ -62,7 +62,7 @@ bool SDMCImporter::IsGood() const { return is_good; } -void SDMCImporter::Abort() { +void SDMCImporter::AbortImporting() { decryptor->Abort(); } @@ -319,10 +319,8 @@ std::vector SDMCImporter::ListContent() const { // Regex for half Title IDs static const std::regex title_regex{"[0-9a-f]{8}"}; -std::tuple> SDMCImporter::LoadTitleData( - const std::string& path) const { - // Remove trailing '/' - const auto sdmc_path = config.sdmc_path.substr(0, config.sdmc_path.size() - 1); +static bool LoadTMD(const std::string& sdmc_path, const std::string& path, SDMCDecryptor& decryptor, + TitleMetadata& out) { std::string title_metadata; const bool ret = FileUtil::ForeachDirectoryEntry( @@ -344,16 +342,26 @@ std::tuple> SDMCImporte }); if (ret) { // TMD not found - return {}; + return false; } if (!FileUtil::Exists(sdmc_path + path + title_metadata)) { // Probably TMD is not directly inside, aborting. - return {}; + return false; } + return out.Load(decryptor.DecryptFile(path + title_metadata)) == ResultStatus::Success; +} + +std::tuple> SDMCImporter::LoadTitleData( + const std::string& path) const { + // Remove trailing '/' + const auto sdmc_path = config.sdmc_path.substr(0, config.sdmc_path.size() - 1); + TitleMetadata tmd; - tmd.Load(decryptor->DecryptFile(path + title_metadata)); + if (!LoadTMD(sdmc_path, path, *decryptor, tmd)) { + return {}; + } const auto boot_content_path = fmt::format("{}{:08x}.app", path, tmd.GetBootContentID()); @@ -390,6 +398,32 @@ std::tuple> SDMCImporte encryption, seed_crypto, smdh.GetIcon(false)}; } +bool SDMCImporter::DumpCXI(const ContentSpecifier& specifier, const std::string& destination, + const ProgressCallback& callback) { + + if (specifier.type != ContentType::Application) { + LOG_ERROR(Core, "Unsupported specifier type {}", static_cast(specifier.type)); + return false; + } + + const auto content_path = fmt::format("/title/{:08x}/{:08x}/content/", specifier.id >> 32, + (specifier.id & 0xFFFFFFFF)); + TitleMetadata tmd; + if (!LoadTMD(config.sdmc_path, content_path, *decryptor, tmd)) { + LOG_ERROR(Core, "Could not load tmd"); + return false; + } + + const auto boot_content_path = + fmt::format("{}{:08x}.app", content_path, tmd.GetBootContentID()); + dump_cxi_ncch = std::make_unique(config.sdmc_path, boot_content_path); + return dump_cxi_ncch->DecryptToFile(destination, callback) == ResultStatus::Success; +} + +void SDMCImporter::AbortDumpCXI() { + dump_cxi_ncch->AbortDecryptToFile(); +} + void SDMCImporter::ListTitle(std::vector& out) const { const auto ProcessDirectory = [this, &out, &sdmc_path = config.sdmc_path](ContentType type, u64 high_id) { diff --git a/src/core/importer.h b/src/core/importer.h index 1985f60..b80f638 100644 --- a/src/core/importer.h +++ b/src/core/importer.h @@ -52,7 +52,7 @@ struct ContentSpecifier { u64 extdata_id; ///< Extdata ID for Applications. EncryptionType encryption = EncryptionType::None; ///< Only for NCCHs. Encryption scheme. bool seed_crypto = false; ///< Only for NCCHs. Whether seed crypto is used. - std::vector icon; ///< Optional. The content's icon. + std::vector icon; ///< Optional. The content's icon. }; /** @@ -70,15 +70,17 @@ struct Config { // The following system files are optional for importing and are only copied so that Citra // will be able to decrypt imported encrypted ROMs. - std::string safe_mode_firm_path; ///< Path to safe mode firm (A folder) (Sysdata 1) - std::string seed_db_path; ///< Path to seeddb.bin (Sysdata 2) - std::string secret_sector_path; ///< Path to secret sector (New3DS only) (Sysdata 3) + std::string safe_mode_firm_path; ///< Path to safe mode firm (A folder) (Sysdata 1) + std::string seed_db_path; ///< Path to seeddb.bin (Sysdata 2) + std::string secret_sector_path; ///< Path to secret sector (New3DS only) (Sysdata 3) // Note: Sysdata 4 is aes_keys.txt (slot0x25KeyX) std::string config_savegame_path; ///< Path to config savegame (Sysdata 5) std::string system_archives_path; ///< Path to system archives. }; +class NCCHContainer; + class SDMCImporter { public: /// (current_size, total_size) @@ -93,24 +95,37 @@ public: ~SDMCImporter(); /** - * Aborts a specific content by its specifier. + * Imports a specific content by its specifier. * Blocks, but can be aborted on another thread if needed. * @return true on success, false otherwise */ bool ImportContent(const ContentSpecifier& specifier, const ProgressCallback& callback = [](std::size_t, std::size_t) {}); + /** + * Aborts current importing. + */ + void AbortImporting(); + + /** + * Dumps a content to CXI. + * Blocks, but can be aborted on another thread. + * @return true on success, false otherwise + */ + bool DumpCXI(const ContentSpecifier& specifier, const std::string& destination, + const ProgressCallback& callback = [](std::size_t, std::size_t) {}); + + /** + * Aborts current CXI dumping. + */ + void AbortDumpCXI(); + /** * Deletes/Cleans up a content. Used for deleting contents that have * not been fully imported. */ void DeleteContent(const ContentSpecifier& specifier); - /** - * Aborts current importing. - */ - void Abort(); - /** * Gets a list of dumpable content specifiers. */ @@ -147,11 +162,15 @@ private: * Required to end with '/'. * @return {name, extdata_id, encryption, seed_crypto, icon} */ - std::tuple> LoadTitleData(const std::string& path) const; + std::tuple> LoadTitleData( + const std::string& path) const; bool is_good{}; Config config; std::unique_ptr decryptor; + + // The NCCH used to dump CXIs. + std::unique_ptr dump_cxi_ncch; }; /** diff --git a/src/core/ncch/ncch_container.cpp b/src/core/ncch/ncch_container.cpp index ccf9480..ab696e1 100644 --- a/src/core/ncch/ncch_container.cpp +++ b/src/core/ncch/ncch_container.cpp @@ -15,10 +15,10 @@ #include "common/common_types.h" #include "common/logging/log.h" #include "core/data_container.h" +#include "core/importer.h" #include "core/key/key.h" #include "core/ncch/ncch_container.h" #include "core/ncch/seed_db.h" -#include "core/quick_decryptor.h" namespace Core { @@ -440,10 +440,13 @@ ResultStatus NCCHContainer::DecryptToFile(const std::string& destination, if (!is_encrypted) { // Simply copy everything - QuickDecryptor decryptor; + file->Seek(0, SEEK_SET); + const auto size = file->GetSize(); decryptor.Reset(size); - decryptor.DecryptAndWriteFile(file, size, dest_file, callback); + + const bool ret = decryptor.DecryptAndWriteFile(file, size, dest_file, callback); + return ret ? ResultStatus::Success : ResultStatus::Error; } // Write NCCH header @@ -463,7 +466,6 @@ ResultStatus NCCHContainer::DecryptToFile(const std::string& destination, return ResultStatus::Error; } - QuickDecryptor decryptor; const auto total_size = file->GetSize() - sizeof(NCCH_Header) - sizeof(ExHeader_Header) - sizeof(ExeFs_Header); decryptor.Reset(total_size); @@ -477,12 +479,19 @@ ResultStatus NCCHContainer::DecryptToFile(const std::string& destination, return true; } + if (aborted.exchange(false)) { + return false; + } file->Seek(written, SEEK_SET); ASSERT_MSG(written <= offset, "Offsets are not in increasing order"); if (!decryptor.DecryptAndWriteFile(file, offset - written, dest_file, callback)) { LOG_ERROR(Core, "Could not write data before {} to {}", name, destination); return false; } + + if (aborted.exchange(false)) { + return false; + } if (!decryptor.DecryptAndWriteFile(file, size, dest_file, callback, decrypt, key, ctr, aes_seek_pos)) { LOG_ERROR(Core, "Could not write {} to {}", name, destination); @@ -536,6 +545,11 @@ ResultStatus NCCHContainer::DecryptToFile(const std::string& destination, return ResultStatus::Success; } +void NCCHContainer::AbortDecryptToFile() { + aborted = true; + decryptor.Abort(); +} + #pragma pack(push, 1) struct RomFSIVFCHeader { u32_le magic; diff --git a/src/core/ncch/ncch_container.h b/src/core/ncch/ncch_container.h index 26afeda..8152852 100644 --- a/src/core/ncch/ncch_container.h +++ b/src/core/ncch/ncch_container.h @@ -13,7 +13,6 @@ #include "common/file_util.h" #include "common/swap.h" #include "core/decryptor.h" -#include "core/importer.h" #include "core/result_status.h" namespace Core { @@ -196,6 +195,8 @@ struct ExHeader_Header { static_assert(sizeof(ExHeader_Header) == 0x800, "ExHeader structure size is wrong"); +enum class EncryptionType; + /** * Helper which implements an interface to deal with NCCH containers which can * contain ExeFS archives or RomFS archives for games or other applications. @@ -272,6 +273,11 @@ public: ResultStatus DecryptToFile(const std::string& destination, const ProgressCallback& callback = [](std::size_t, std::size_t) {}); + /** + * Aborts DecryptToFile. Simply aborts the decryptor. + */ + void AbortDecryptToFile(); + NCCH_Header ncch_header; ExHeader_Header exheader_header; ExeFs_Header exefs_header; @@ -298,6 +304,10 @@ private: std::string filepath; std::shared_ptr file; std::shared_ptr exefs_file; + + // Used for DecryptToFile + QuickDecryptor decryptor; + std::atomic_bool aborted{false}; }; /** diff --git a/src/frontend/CMakeLists.txt b/src/frontend/CMakeLists.txt index 4bcf674..8aea3b9 100644 --- a/src/frontend/CMakeLists.txt +++ b/src/frontend/CMakeLists.txt @@ -9,11 +9,13 @@ endif() file(GLOB_RECURSE THEMES ${PROJECT_SOURCE_DIR}/dist/themes/*) add_executable(threeSD + helpers/import_job.cpp + helpers/import_job.h + helpers/progressive_job.cpp + helpers/progressive_job.h import_dialog.cpp import_dialog.h import_dialog.ui - import_job.cpp - import_job.h main.cpp main.h main.ui diff --git a/src/frontend/import_job.cpp b/src/frontend/helpers/import_job.cpp similarity index 93% rename from src/frontend/import_job.cpp rename to src/frontend/helpers/import_job.cpp index 836daa6..ade1371 100644 --- a/src/frontend/import_job.cpp +++ b/src/frontend/helpers/import_job.cpp @@ -2,9 +2,7 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. -#include "frontend/import_job.h" - -#include "common/assert.h" +#include "frontend/helpers/import_job.h" ImportJob::ImportJob(QObject* parent, Core::SDMCImporter& importer_, std::vector contents_) @@ -39,5 +37,5 @@ void ImportJob::run() { void ImportJob::Cancel() { cancelled.store(true); - importer.Abort(); + importer.AbortImporting(); } diff --git a/src/frontend/import_job.h b/src/frontend/helpers/import_job.h similarity index 100% rename from src/frontend/import_job.h rename to src/frontend/helpers/import_job.h diff --git a/src/frontend/helpers/progressive_job.cpp b/src/frontend/helpers/progressive_job.cpp new file mode 100644 index 0000000..0439fe4 --- /dev/null +++ b/src/frontend/helpers/progressive_job.cpp @@ -0,0 +1,27 @@ +// Copyright 2020 Pengfei Zhu +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "frontend/helpers/progressive_job.h" + +ProgressiveJob::ProgressiveJob(QObject* parent, const ExecuteFunc& execute_, + const AbortFunc& abort_) + : QThread(parent), execute(execute_), abort(abort_) {} + +ProgressiveJob::~ProgressiveJob() = default; + +void ProgressiveJob::run() { + const bool ret = execute( + [this](std::size_t current, std::size_t total) { emit ProgressUpdated(current, total); }); + + if (ret || canceled) { + emit Completed(); + } else { + emit ErrorOccured(); + } +} + +void ProgressiveJob::Cancel() { + canceled = true; + abort(); +} diff --git a/src/frontend/helpers/progressive_job.h b/src/frontend/helpers/progressive_job.h new file mode 100644 index 0000000..da5602a --- /dev/null +++ b/src/frontend/helpers/progressive_job.h @@ -0,0 +1,36 @@ +// Copyright 2019 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include "common/common_types.h" + +/** + * Lightweight wrapper around QThread, for easy use with progressive jobs. + */ +class ProgressiveJob : public QThread { + Q_OBJECT + +public: + using ProgressCallback = std::function; + using ExecuteFunc = std::function; + using AbortFunc = std::function; + + explicit ProgressiveJob(QObject* parent, const ExecuteFunc& execute, const AbortFunc& abort); + ~ProgressiveJob() override; + + void run() override; + void Cancel(); + +signals: + void ProgressUpdated(u64 current, u64 total); + void Completed(); + void ErrorOccured(); + +private: + ExecuteFunc execute; + AbortFunc abort; + bool canceled{}; +}; diff --git a/src/frontend/import_dialog.cpp b/src/frontend/import_dialog.cpp index d05065e..ff9491b 100644 --- a/src/frontend/import_dialog.cpp +++ b/src/frontend/import_dialog.cpp @@ -6,7 +6,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -14,8 +16,9 @@ #include #include "common/logging/log.h" #include "common/scope_exit.h" +#include "frontend/helpers/import_job.h" +#include "frontend/helpers/progressive_job.h" #include "frontend/import_dialog.h" -#include "frontend/import_job.h" #include "ui_import_dialog.h" QString ReadableByteSize(qulonglong size) { @@ -119,6 +122,8 @@ ImportDialog::ImportDialog(QWidget* parent, const Core::Config& config) ui->main->setColumnWidth(2, width() * 0.14); ui->main->setColumnWidth(3, width() * 0.17); ui->main->setColumnWidth(4, width() * 0.08); + + connect(ui->main, &QTreeWidget::customContextMenuRequested, this, &ImportDialog::OnContextMenu); } ImportDialog::~ImportDialog() = default; @@ -502,3 +507,92 @@ void ImportDialog::StartImporting() { job->start(); } + +Core::ContentSpecifier ImportDialog::SpecifierFromItem(QTreeWidgetItem* item) const { + const auto* checkBox = static_cast(ui->main->itemWidget(item, 0)); + return contents[checkBox->property("id").toInt()]; +} + +void ImportDialog::OnContextMenu(const QPoint& point) { + QTreeWidgetItem* item = ui->main->itemAt(point.x(), point.y()); + if (!item) { + return; + } + + const bool title_view = ui->title_view_button->isChecked(); + + QMenu context_menu; + if (item->parent()) { // Second level + const auto& specifier = SpecifierFromItem(item); + if (specifier.type != Core::ContentType::Application) { + return; + } + + QAction* dump_cxi = context_menu.addAction(tr("Dump CXI file")); + connect(dump_cxi, &QAction::triggered, [this, specifier] { StartDumpingCXI(specifier); }); + } else { // Top level + if (!title_view) { + return; + } + + for (int i = 0; i < item->childCount(); ++i) { + const auto& specifier = SpecifierFromItem(item->child(i)); + if (specifier.type == Core::ContentType::Application) { + QAction* dump_base_cxi = context_menu.addAction(tr("Dump Base CXI file")); + connect(dump_base_cxi, &QAction::triggered, + [this, specifier] { StartDumpingCXI(specifier); }); + break; + } + // TODO: Add updates, etc + } + } + context_menu.exec(ui->main->viewport()->mapToGlobal(point)); +} + +void ImportDialog::StartDumpingCXI(const Core::ContentSpecifier& specifier) { + const QString path = QFileDialog::getSaveFileName(this, tr("Dump CXI file"), last_dump_cxi_path, + tr("CTR Executable Image (*.CXI)")); + if (path.isEmpty()) { + return; + } + last_dump_cxi_path = QFileInfo(path).path(); + + // Try to map total_size to int range + // This is equal to ceil(total_size / INT_MAX) + const u64 multiplier = (specifier.maximum_size + std::numeric_limits::max() - 1) / + std::numeric_limits::max(); + + auto* dialog = new QProgressDialog(tr("Initializing..."), tr("Cancel"), 0, + static_cast(specifier.maximum_size / multiplier), this); + dialog->setWindowModality(Qt::WindowModal); + dialog->setMinimumDuration(0); + dialog->setValue(0); + + auto* job = new ProgressiveJob( + this, + [this, specifier, path](const ProgressiveJob::ProgressCallback& callback) { + if (!importer.DumpCXI(specifier, path.toStdString(), callback)) { + FileUtil::Delete(path.toStdString()); + return false; + } + return true; + }, + [this] { importer.AbortDumpCXI(); }); + + connect(job, &ProgressiveJob::ProgressUpdated, this, + [this, specifier, dialog, multiplier](u64 current, u64 total) { + dialog->setValue(static_cast(current / multiplier)); + dialog->setLabelText(tr("%1 / %2") + .arg(ReadableByteSize(current)) + .arg(ReadableByteSize(specifier.maximum_size))); + }); + connect(job, &ProgressiveJob::ErrorOccured, this, [this, dialog] { + QMessageBox::critical(this, tr("threeSD"), tr("Failed to dump CXI!")); + dialog->hide(); + }); + connect(job, &ProgressiveJob::Completed, this, + [this, dialog] { dialog->setValue(dialog->maximum()); }); + connect(dialog, &QProgressDialog::canceled, this, [this, job] { job->Cancel(); }); + + job->start(); +} diff --git a/src/frontend/import_dialog.h b/src/frontend/import_dialog.h index 2231a8a..cdcfcdc 100644 --- a/src/frontend/import_dialog.h +++ b/src/frontend/import_dialog.h @@ -10,6 +10,7 @@ #include #include #include "core/importer.h" +#include "core/ncch/ncch_container.h" class QTreeWidgetItem; @@ -38,6 +39,12 @@ private: std::size_t id, QString replace_name = {}, QPixmap replace_icon = {}); + Core::ContentSpecifier SpecifierFromItem(QTreeWidgetItem* item) const; + void OnContextMenu(const QPoint& point); + void StartDumpingCXI(const Core::ContentSpecifier& content); + Core::NCCHContainer dump_cxi_container; // NCCH container used for dumping CXI + QString last_dump_cxi_path; // Used for recording last path in StartDumpingCXI + std::unique_ptr ui; std::string user_path; diff --git a/src/frontend/import_dialog.ui b/src/frontend/import_dialog.ui index 2fedaee..1ffd771 100644 --- a/src/frontend/import_dialog.ui +++ b/src/frontend/import_dialog.ui @@ -41,6 +41,9 @@ + + Qt::CustomContextMenu +