diff --git a/src/core/decryptor.h b/src/core/decryptor.h index b10802b..15e2f48 100644 --- a/src/core/decryptor.h +++ b/src/core/decryptor.h @@ -7,6 +7,8 @@ #include #include #include "common/common_types.h" +#include "common/file_util.h" +#include "common/logging/log.h" #include "core/quick_decryptor.h" namespace Core { diff --git a/src/frontend/CMakeLists.txt b/src/frontend/CMakeLists.txt index 851a735..4bcf674 100644 --- a/src/frontend/CMakeLists.txt +++ b/src/frontend/CMakeLists.txt @@ -17,6 +17,12 @@ add_executable(threeSD main.cpp main.h main.ui + select_files_dialog.cpp + select_files_dialog.h + select_files_dialog.ui + utilities.cpp + utilities.h + utilities.ui ${THEMES} ) diff --git a/src/frontend/main.cpp b/src/frontend/main.cpp index 55849dd..0890baf 100644 --- a/src/frontend/main.cpp +++ b/src/frontend/main.cpp @@ -11,6 +11,7 @@ #include "common/file_util.h" #include "frontend/import_dialog.h" #include "frontend/main.h" +#include "frontend/utilities.h" #include "ui_main.h" #ifdef __APPLE__ @@ -47,6 +48,10 @@ MainDialog::MainDialog(QWidget* parent) : QDialog(parent), ui(std::make_uniqueutilitiesButton, &QPushButton::clicked, [this] { + UtilitiesDialog dialog(this); + dialog.exec(); + }); connect(ui->buttonBox, &QDialogButtonBox::clicked, [this](QAbstractButton* button) { if (button == ui->buttonBox->button(QDialogButtonBox::StandardButton::Reset)) { diff --git a/src/frontend/main.ui b/src/frontend/main.ui index ef0a69d..9d8fc63 100644 --- a/src/frontend/main.ui +++ b/src/frontend/main.ui @@ -51,6 +51,13 @@ + + + + Utilities... + + + diff --git a/src/frontend/select_files_dialog.cpp b/src/frontend/select_files_dialog.cpp new file mode 100644 index 0000000..107f881 --- /dev/null +++ b/src/frontend/select_files_dialog.cpp @@ -0,0 +1,53 @@ +// Copyright 2020 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include "frontend/select_files_dialog.h" +#include "ui_select_files_dialog.h" + +SelectFilesDialog::SelectFilesDialog(QWidget* parent, bool source_is_dir_, bool destination_is_dir_) + : QDialog(parent), ui(std::make_unique()), source_is_dir(source_is_dir_), + destination_is_dir(destination_is_dir_) { + + ui->setupUi(this); + + connect(ui->buttonBox, &QDialogButtonBox::accepted, [this] { + if (ui->source->text().isEmpty() || ui->destination->text().isEmpty()) { + QMessageBox::warning(this, tr("Warning"), tr("Please fill in all the fields.")); + return; + } + accept(); + }); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &SelectFilesDialog::reject); + + connect(ui->sourceExplore, &QToolButton::clicked, [this] { + QString path; + if (source_is_dir) { + path = QFileDialog::getExistingDirectory(this, tr("Select Directory")); + } else { + path = QFileDialog::getOpenFileName(this, tr("Select File")); + } + if (!path.isEmpty()) { + ui->source->setText(path); + } + }); + connect(ui->destinationExplore, &QToolButton::clicked, [this] { + QString path; + if (destination_is_dir) { + path = QFileDialog::getExistingDirectory(this, tr("Select Directory")); + } else { + path = QFileDialog::getSaveFileName(this, tr("Select File")); + } + if (!path.isEmpty()) { + ui->destination->setText(path); + } + }); +} + +SelectFilesDialog::~SelectFilesDialog() = default; + +std::pair SelectFilesDialog::GetResults() const { + return {ui->source->text(), ui->destination->text()}; +} diff --git a/src/frontend/select_files_dialog.h b/src/frontend/select_files_dialog.h new file mode 100644 index 0000000..ebc4510 --- /dev/null +++ b/src/frontend/select_files_dialog.h @@ -0,0 +1,29 @@ +// Copyright 2020 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include + +class QWidget; + +namespace Ui { +class SelectFilesDialog; +} + +class SelectFilesDialog : public QDialog { + Q_OBJECT + +public: + explicit SelectFilesDialog(QWidget* parent, bool source_is_dir, bool destination_is_dir); + ~SelectFilesDialog() override; + + std::pair GetResults() const; + +private: + std::unique_ptr ui; + bool source_is_dir; // Whether Source should be a directory + bool destination_is_dir; // Whether Destination should be a directory +}; diff --git a/src/frontend/select_files_dialog.ui b/src/frontend/select_files_dialog.ui new file mode 100644 index 0000000..412cbbd --- /dev/null +++ b/src/frontend/select_files_dialog.ui @@ -0,0 +1,76 @@ + + + SelectFilesDialog + + + + 0 + 0 + 600 + 120 + + + + Select Files + + + + + + + + Source: + + + + + + + + + + + 0 + 0 + + + + ... + + + + + + + Destination: + + + + + + + + + + + 0 + 0 + + + + ... + + + + + + + + + QDialogButtonBox::Ok|QDialogButtonBox::Cancel + + + + + + diff --git a/src/frontend/utilities.cpp b/src/frontend/utilities.cpp new file mode 100644 index 0000000..ef7f74b --- /dev/null +++ b/src/frontend/utilities.cpp @@ -0,0 +1,297 @@ +// Copyright 2020 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include "core/data_container.h" +#include "core/decryptor.h" +#include "core/inner_fat.h" +#include "core/key/key.h" +#include "core/ncch/ncch_container.h" +#include "frontend/select_files_dialog.h" +#include "frontend/utilities.h" +#include "ui_utilities.h" + +UtilitiesDialog::UtilitiesDialog(QWidget* parent) + : QDialog(parent), ui(std::make_unique()) { + + ui->setupUi(this); + + connect(ui->useSdDecryption, &QCheckBox::clicked, [this] { + const bool checked = ui->useSdDecryption->isChecked(); + + ui->boot9Path->setEnabled(checked); + ui->boot9PathExplore->setEnabled(checked); + ui->movableSedPath->setEnabled(checked); + ui->movableSedPathExplore->setEnabled(checked); + ui->sdmcPath->setEnabled(checked); + ui->sdmcPathExplore->setEnabled(checked); + + // First hide both, to avoid resizing the dialog + ui->sdDecryptionLabel->setVisible(false); + ui->sdDecryptionDisabledLabel->setVisible(false); + ui->sdDecryptionLabel->setVisible(checked); + ui->sdDecryptionDisabledLabel->setVisible(!checked); + ui->sdDecryption->setEnabled(checked); + + ui->savedataExtractionLabel->setVisible(false); + ui->savedataExtractionDisabledLabel->setVisible(false); + ui->savedataExtractionLabel->setVisible(checked); + ui->savedataExtractionDisabledLabel->setVisible(!checked); + ui->savedataExtraction->setEnabled(checked); + + ui->extdataExtractionLabel->setVisible(false); + ui->extdataExtractionDisabledLabel->setVisible(false); + ui->extdataExtractionLabel->setVisible(checked); + ui->extdataExtractionDisabledLabel->setVisible(!checked); + ui->extdataExtraction->setEnabled(checked); + + ui->romfsExtractionLabel->setVisible(false); + ui->romfsExtractionDisabledLabel->setVisible(false); + ui->romfsExtractionLabel->setVisible(!checked); + ui->romfsExtractionDisabledLabel->setVisible(checked); + ui->romfsExtraction->setEnabled(!checked); + }); + + connect(ui->boot9PathExplore, &QToolButton::clicked, [this] { + const QString path = QFileDialog::getOpenFileName(this, tr("Select File")); + if (!path.isEmpty()) { + ui->boot9Path->setText(path); + } + }); + connect(ui->movableSedPathExplore, &QToolButton::clicked, [this] { + const QString path = QFileDialog::getOpenFileName(this, tr("Select File")); + if (!path.isEmpty()) { + ui->movableSedPath->setText(path); + } + }); + connect(ui->sdmcPathExplore, &QToolButton::clicked, [this] { + const QString path = QFileDialog::getExistingDirectory(this, tr("Select Directory")); + if (!path.isEmpty()) { + ui->sdmcPath->setText(path); + } + }); + + connect(ui->sdDecryption, &QPushButton::clicked, this, &UtilitiesDialog::SDDecryptionTool); + connect(ui->savedataExtraction, &QPushButton::clicked, this, + &UtilitiesDialog::SaveDataExtractionTool); + connect(ui->extdataExtraction, &QPushButton::clicked, this, + &UtilitiesDialog::ExtdataExtractionTool); + connect(ui->romfsExtraction, &QPushButton::clicked, this, + &UtilitiesDialog::RomFSExtractionTool); +} + +UtilitiesDialog::~UtilitiesDialog() = default; + +std::pair UtilitiesDialog::GetFilePaths(bool source_is_dir, + bool destination_is_dir) { + + SelectFilesDialog dialog(this, source_is_dir, destination_is_dir); + if (dialog.exec() == QDialog::Accepted) { + return dialog.GetResults(); + } else { + return {}; + } +} + +bool UtilitiesDialog::LoadSDKeys() { + if (ui->boot9Path->text().isEmpty() || ui->movableSedPath->text().isEmpty()) { + QMessageBox::critical(this, tr("Error"), + tr("Please select boot9.bin and movable.sed paths.")); + return false; + } + if (ui->sdmcPath->text().isEmpty()) { + QMessageBox::critical(this, tr("Error"), + tr("Please select SDMC root (\"Nintendo 3DS/<ID0>/<ID1>\").")); + return false; + } + + Core::Key::ClearKeys(); + Core::Key::LoadBootromKeys(ui->boot9Path->text().toStdString()); + Core::Key::LoadMovableSedKeys(ui->movableSedPath->text().toStdString()); + + if (!Core::Key::IsNormalKeyAvailable(Core::Key::SDKey)) { + LOG_ERROR(Core, "SDKey is not available"); + QMessageBox::critical(this, tr("Error"), + tr("Could not load SD Key. Please check your files.")); + return false; + } + return true; +} + +void UtilitiesDialog::ShowProgressDialog(std::function operation) { + auto* dialog = new QProgressDialog(tr("Processing..."), tr("Cancel"), 0, 0, this); + dialog->setWindowModality(Qt::WindowModal); + dialog->setCancelButton(nullptr); + dialog->setMinimumDuration(0); + dialog->setValue(0); + + using FutureWatcher = QFutureWatcher; + auto* future_watcher = new FutureWatcher(this); + connect(future_watcher, &FutureWatcher::finished, this, [this, dialog] { + dialog->hide(); + ShowResult(); + }); + + auto future = QtConcurrent::run([operation, this] { result = operation(); }); + future_watcher->setFuture(future); +} + +std::tuple UtilitiesDialog::GetSDMCRoot(const QString& source) { + QString sdmc_root = ui->sdmcPath->text().replace(QLatin1Char{'\\'}, QLatin1Char{'/'}); + if (!sdmc_root.endsWith(QLatin1Char{'/'})) { + sdmc_root.append(QLatin1Char{'/'}); + } + if (!source.startsWith(sdmc_root)) { + QMessageBox::critical(this, tr("Error"), tr("The file selected is not in SDMC root.")); + return {false, "", ""}; + } + const std::string relative_source = + source.toStdString().substr(sdmc_root.toStdString().size() - 1); + + return {true, sdmc_root.toStdString(), relative_source}; +} + +void UtilitiesDialog::SDDecryptionTool() { + if (!LoadSDKeys()) { + return; + } + const auto& [source, destination] = GetFilePaths(false, false); + if (source.isEmpty() || destination.isEmpty()) { + return; + } + + const auto& [success, sdmc_root, relative_source] = GetSDMCRoot(source); + if (!success) { + return; + } + // TODO: Add Progress reporting + ShowProgressDialog([sdmc_root, relative_source, destination] { + Core::SDMCDecryptor decryptor(sdmc_root); + return decryptor.DecryptAndWriteFile(relative_source, destination.toStdString()); + }); +} + +void UtilitiesDialog::SaveDataExtractionTool() { + const bool decryption = ui->useSdDecryption->isChecked(); + if (decryption && !LoadSDKeys()) { + return; + } + const auto& [source, destination] = GetFilePaths(false, true); + if (source.isEmpty() || destination.isEmpty()) { + return; + } + + if (decryption) { + const auto& [success, sdmc_root, relative_source] = GetSDMCRoot(source); + if (!success) { + return; + } + + // TODO: Add Progress reporting + ShowProgressDialog([sdmc_root, relative_source, source, destination] { + const auto size = FileUtil::GetSize(source.toStdString()); + std::vector data(size); + Core::SDMCFile file(sdmc_root, relative_source, "rb"); + if (file.ReadBytes(data.data(), size) != size) { + return false; + } + + Core::DataContainer container(data); + if (!container.IsGood()) { + return false; + } + + Core::SDSavegame save(std::move(container.GetIVFCLevel4Data())); + if (!save.IsGood()) { + return false; + } + + return save.Extract(destination.toStdString()); + }); + } else { + // TODO: Add Progress reporting + ShowProgressDialog([source, destination] { + const auto size = FileUtil::GetSize(source.toStdString()); + std::vector data(size); + FileUtil::IOFile file(source.toStdString(), "rb"); + if (file.ReadBytes(data.data(), size) != size) { + return false; + } + + Core::DataContainer container(data); + if (!container.IsGood()) { + return false; + } + + Core::SDSavegame save(std::move(container.GetIVFCLevel4Data())); + if (!save.IsGood()) { + return false; + } + + return save.Extract(destination.toStdString()); + }); + } +} + +void UtilitiesDialog::ExtdataExtractionTool() { + if (!LoadSDKeys()) { + return; + } + const auto& [source, destination] = GetFilePaths(true, true); + if (source.isEmpty() || destination.isEmpty()) { + return; + } + + const auto& [success, sdmc_root, relative_source] = GetSDMCRoot(source); + if (!success) { + return; + } + // TODO: Add Progress reporting + ShowProgressDialog([sdmc_root, relative_source, destination] { + Core::SDMCDecryptor decryptor(sdmc_root); + Core::SDExtdata extdata(relative_source, decryptor); + if (!extdata.IsGood()) { + return false; + } + + return extdata.Extract(destination.toStdString()); + }); +} + +void UtilitiesDialog::RomFSExtractionTool() { + const auto& [source, destination] = GetFilePaths(false, false); + if (source.isEmpty() || destination.isEmpty()) { + return; + } + + ShowProgressDialog([source, destination] { + const auto size = FileUtil::GetSize(source.toStdString()); + std::vector data(size); + FileUtil::IOFile src_file(source.toStdString(), "rb"); + if (src_file.ReadBytes(data.data(), size) != size) { + return false; + } + + const auto& shared_romfs = Core::LoadSharedRomFS(data); + FileUtil::IOFile dest_file(destination.toStdString(), "wb"); + if (dest_file.WriteBytes(shared_romfs.data(), shared_romfs.size()) != shared_romfs.size()) { + return false; + } + + return true; + }); +} + +void UtilitiesDialog::ShowResult() { + if (result) { + QMessageBox::information(this, tr("Success"), tr("Operation completed successfully.")); + } else { + QMessageBox::critical(this, tr("Error"), + tr("An error occured while performing the operation.")); + } +} diff --git a/src/frontend/utilities.h b/src/frontend/utilities.h new file mode 100644 index 0000000..d84a53e --- /dev/null +++ b/src/frontend/utilities.h @@ -0,0 +1,49 @@ +// Copyright 2020 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include + +class QWidget; + +namespace Ui { +class UtilitiesDialog; +} + +class UtilitiesDialog : public QDialog { + Q_OBJECT + +public: + explicit UtilitiesDialog(QWidget* parent); + ~UtilitiesDialog() override; + +private: + /** + * Open a dialog to ask the user for source and destination paths. + * @return {source, destination} + */ + std::pair GetFilePaths(bool source_is_dir, bool destination_is_dir); + + bool LoadSDKeys(); + + void ShowProgressDialog(std::function operation); + + /** + * Gets SDMC root, and relative source path. + * @return {success, sdmc root, relative source path} + */ + std::tuple GetSDMCRoot(const QString& source); + + void SDDecryptionTool(); + void SaveDataExtractionTool(); + void ExtdataExtractionTool(); + void RomFSExtractionTool(); + void ShowResult(); + + bool result = false; /// Result of the last operation. + std::unique_ptr ui; +}; diff --git a/src/frontend/utilities.ui b/src/frontend/utilities.ui new file mode 100644 index 0000000..6fa7e14 --- /dev/null +++ b/src/frontend/utilities.ui @@ -0,0 +1,277 @@ + + + UtilitiesDialog + + + + 0 + 0 + 800 + 480 + + + + threeSD Utilities + + + + + + Encryption + + + + + + Use SD Decryption (check this when your files are directly from SD Card) + + + true + + + + + + + + + boot9.bin + + + + + + + + + + + 0 + 0 + + + + ... + + + + + + + movable.sed + + + + + + + + + + + 0 + 0 + + + + ... + + + + + + + SDMC Root + + + Path to "Nintendo 3DS/<ID0>/<ID1>" folder. + + + + + + + + + + + 0 + 0 + + + + ... + + + + + + + + + + + + SD Decryption + + + + + + Decrypt files from your SD Card. + + + + + + + SD Decryption must be enabled to use this tool. + + + false + + + + + + + Qt::Horizontal + + + + + + + Open... + + + + + + + + + + Save Data Extraction + + + + + + Extract 3DS SD Savegames. + + + + + + + SD Decryption must be enabled to use this tool. + + + false + + + + + + + Qt::Horizontal + + + + + + + Open... + + + + + + + + + + Extdata Extraction + + + + + + Extract 3DS Extra Data. + + + + + + + SD Decryption must be enabled to use this tool. + + + false + + + + + + + Qt::Horizontal + + + + + + + Open... + + + + + + + + + + RomFS Extraction + + + + + + Extract shared RomFS from NCCH. Useful for System Archives. + + + false + + + + + + + SD Decryption must be disabled to use this tool. + + + + + + + Qt::Horizontal + + + + + + + Open... + + + false + + + + + + + + +