core, frontend: Add 'Dump CXI file' option

Right click on an application in the Select Contents dialog.
This commit is contained in:
zhupengfei
2020-05-02 00:06:46 +08:00
parent 2c4dd84d49
commit 24bdf0a156
12 changed files with 275 additions and 31 deletions
+42 -8
View File
@@ -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<ContentSpecifier> SDMCImporter::ListContent() const {
// Regex for half Title IDs
static const std::regex title_regex{"[0-9a-f]{8}"};
std::tuple<std::string, u64, EncryptionType, bool, std::vector<u16>> 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<std::string, u64, EncryptionType, bool, std::vector<u16>> 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<std::string, u64, EncryptionType, bool, std::vector<u16>> 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<std::string, u64, EncryptionType, bool, std::vector<u16>> 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<int>(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<NCCHContainer>(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<ContentSpecifier>& out) const {
const auto ProcessDirectory = [this, &out, &sdmc_path = config.sdmc_path](ContentType type,
u64 high_id) {
+30 -11
View File
@@ -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<u16> icon; ///< Optional. The content's icon.
std::vector<u16> 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<std::string, u64, EncryptionType, bool, std::vector<u16>> LoadTitleData(const std::string& path) const;
std::tuple<std::string, u64, EncryptionType, bool, std::vector<u16>> LoadTitleData(
const std::string& path) const;
bool is_good{};
Config config;
std::unique_ptr<SDMCDecryptor> decryptor;
// The NCCH used to dump CXIs.
std::unique_ptr<NCCHContainer> dump_cxi_ncch;
};
/**
+18 -4
View File
@@ -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<SDMCFile, FileUtil::IOFile> 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<SDMCFile, FileUtil::IOFile> 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;
+11 -1
View File
@@ -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<SDMCFile> file;
std::shared_ptr<SDMCFile> exefs_file;
// Used for DecryptToFile
QuickDecryptor<SDMCFile, FileUtil::IOFile> decryptor;
std::atomic_bool aborted{false};
};
/**
+4 -2
View File
@@ -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
@@ -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<Core::ContentSpecifier> contents_)
@@ -39,5 +37,5 @@ void ImportJob::run() {
void ImportJob::Cancel() {
cancelled.store(true);
importer.Abort();
importer.AbortImporting();
}
+27
View File
@@ -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();
}
+36
View File
@@ -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 <QThread>
#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<void(std::size_t, std::size_t)>;
using ExecuteFunc = std::function<bool(const ProgressCallback&)>;
using AbortFunc = std::function<void()>;
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{};
};
+95 -1
View File
@@ -6,7 +6,9 @@
#include <cmath>
#include <unordered_map>
#include <QCheckBox>
#include <QFileDialog>
#include <QFutureWatcher>
#include <QMenu>
#include <QMessageBox>
#include <QProgressDialog>
#include <QPushButton>
@@ -14,8 +16,9 @@
#include <QtConcurrent/QtConcurrentRun>
#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<QCheckBox*>(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<int>::max() - 1) /
std::numeric_limits<int>::max();
auto* dialog = new QProgressDialog(tr("Initializing..."), tr("Cancel"), 0,
static_cast<int>(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<int>(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();
}
+7
View File
@@ -10,6 +10,7 @@
#include <QDialog>
#include <QPixmap>
#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::ImportDialog> ui;
std::string user_path;
+3
View File
@@ -41,6 +41,9 @@
</item>
<item>
<widget class="QTreeWidget" name="main">
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
<column>
<property name="text">
<string/>