mirror of
https://github.com/Dark98/threeSD.git
synced 2026-07-03 00:38:58 +00:00
core, frontend: Better progress reporter
A new "quick" decryptor is implemented. This is not really much faster (not slower either) but provides the benefit of being able to report progress on a single file. The frontend is updated accordingly to support this feature.
This commit is contained in:
@@ -49,7 +49,6 @@ Make sure the SD card is properly recognized and shows up as a disk.
|
||||
* The `System Data` group contains important data that is necessary for your imported games to run. You should definitely import the contents there, if they do not exist yet.
|
||||
1. After you've finished your selection, click `OK`. You should now see a progress dialog; wait a while until your contents are imported.
|
||||
* The time will depend on how big your contents are, as well as your CPU processing power and (mainly) disk I/O speeds.
|
||||
* The progress is only updated every time a content has been *fully* imported. If you see it stall for a long time, it just means that a very large content is being processed. Just be more patient and do not worry. You can also check the disk usage in `Task Manager`.
|
||||
|
||||
### What to do next
|
||||
|
||||
@@ -66,7 +65,6 @@ If you have any game cartidges, and would like to dump them as well, visit [this
|
||||
* UI improvements
|
||||
* Better error messages
|
||||
* Beautiful icons
|
||||
* Better progress indicator
|
||||
* Bug fixes
|
||||
* Clear all the `TODO`s in the code
|
||||
* Wireless transfer (probably FTP?)
|
||||
|
||||
@@ -13,6 +13,7 @@ add_library(common STATIC
|
||||
string_util.cpp
|
||||
string_util.h
|
||||
swap.h
|
||||
thread.h
|
||||
)
|
||||
|
||||
target_link_libraries(common PUBLIC fmt)
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project
|
||||
// 2019 threeSD Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
namespace Common {
|
||||
|
||||
class Event {
|
||||
public:
|
||||
void Set() {
|
||||
std::lock_guard lk{mutex};
|
||||
if (!is_set) {
|
||||
is_set = true;
|
||||
condvar.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
void Wait() {
|
||||
std::unique_lock lk{mutex};
|
||||
condvar.wait(lk, [&] { return is_set; });
|
||||
is_set = false;
|
||||
}
|
||||
|
||||
template <class Duration>
|
||||
bool WaitFor(const std::chrono::duration<Duration>& time) {
|
||||
std::unique_lock lk(mutex);
|
||||
if (!condvar.wait_for(lk, time, [this] { return is_set; }))
|
||||
return false;
|
||||
is_set = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
template <class Clock, class Duration>
|
||||
bool WaitUntil(const std::chrono::time_point<Clock, Duration>& time) {
|
||||
std::unique_lock lk{mutex};
|
||||
if (!condvar.wait_until(lk, time, [this] { return is_set; }))
|
||||
return false;
|
||||
is_set = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Reset() {
|
||||
std::unique_lock lk{mutex};
|
||||
// no other action required, since wait loops on the predicate and any lingering signal will
|
||||
// get cleared on the first iteration
|
||||
is_set = false;
|
||||
}
|
||||
|
||||
private:
|
||||
bool is_set = false;
|
||||
std::condition_variable condvar;
|
||||
std::mutex mutex;
|
||||
};
|
||||
|
||||
class Barrier {
|
||||
public:
|
||||
explicit Barrier(std::size_t count_) : count(count_) {}
|
||||
|
||||
/// Blocks until all "count" threads have called Sync()
|
||||
void Sync() {
|
||||
std::unique_lock lk{mutex};
|
||||
const std::size_t current_generation = generation;
|
||||
|
||||
if (++waiting == count) {
|
||||
generation++;
|
||||
waiting = 0;
|
||||
condvar.notify_all();
|
||||
} else {
|
||||
condvar.wait(lk,
|
||||
[this, current_generation] { return current_generation != generation; });
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t Generation() const {
|
||||
std::unique_lock lk(mutex);
|
||||
return generation;
|
||||
}
|
||||
|
||||
private:
|
||||
std::condition_variable condvar;
|
||||
mutable std::mutex mutex;
|
||||
std::size_t count;
|
||||
std::size_t waiting = 0;
|
||||
std::size_t generation = 0; // Incremented once each time the barrier is used
|
||||
};
|
||||
|
||||
} // namespace Common
|
||||
@@ -11,6 +11,8 @@ add_library(core STATIC
|
||||
key/arithmetic128.h
|
||||
key/key.cpp
|
||||
key/key.h
|
||||
quick_decryptor.cpp
|
||||
quick_decryptor.h
|
||||
)
|
||||
|
||||
target_link_libraries(core PRIVATE common cryptopp)
|
||||
|
||||
+9
-22
@@ -17,7 +17,9 @@
|
||||
|
||||
namespace Core {
|
||||
|
||||
SDMCDecryptor::SDMCDecryptor(const std::string& root_folder_) : root_folder(root_folder_) {
|
||||
SDMCDecryptor::SDMCDecryptor(const std::string& root_folder_)
|
||||
: root_folder(root_folder_), quick_decryptor(root_folder) {
|
||||
|
||||
ASSERT_MSG(Key::IsNormalKeyAvailable(Key::SDKey),
|
||||
"SD Key must be available in order to decrypt");
|
||||
|
||||
@@ -47,28 +49,13 @@ std::array<u8, 16> GetFileCTR(const std::string& path) {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool SDMCDecryptor::DecryptAndWriteFile(const std::string& source,
|
||||
const std::string& destination) const {
|
||||
auto ctr = GetFileCTR(source);
|
||||
auto key = Key::GetNormalKey(Key::SDKey);
|
||||
CryptoPP::CTR_Mode<CryptoPP::AES>::Decryption aes;
|
||||
aes.SetKeyWithIV(key.data(), key.size(), ctr.data());
|
||||
bool SDMCDecryptor::DecryptAndWriteFile(const std::string& source, const std::string& destination,
|
||||
const QuickDecryptor::ProgressCallback& callback) {
|
||||
return quick_decryptor.DecryptAndWriteFile(source, destination, callback);
|
||||
}
|
||||
|
||||
if (!FileUtil::CreateFullPath(destination)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string absolute_source = root_folder + source;
|
||||
try {
|
||||
CryptoPP::FileSource(absolute_source.c_str(), true,
|
||||
new CryptoPP::StreamTransformationFilter(
|
||||
aes, new CryptoPP::FileSink(destination.c_str(), true)),
|
||||
true);
|
||||
} catch (CryptoPP::Exception& e) {
|
||||
LOG_ERROR(Core, "Error decrypting and writing file: {}", e.what());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
void SDMCDecryptor::Abort() {
|
||||
quick_decryptor.Abort();
|
||||
}
|
||||
|
||||
std::vector<u8> SDMCDecryptor::DecryptFile(const std::string& source) const {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "common/common_types.h"
|
||||
#include "core/quick_decryptor.h"
|
||||
|
||||
namespace Core {
|
||||
|
||||
@@ -22,11 +23,17 @@ public:
|
||||
|
||||
/**
|
||||
* Decrypts a file from the SD card and writes it into another file.
|
||||
* This function blocks, but can be aborted with the Abort() function (would return false)
|
||||
*
|
||||
* @param source Path to the file relative to the root folder, starting with "/".
|
||||
* @param destination Path to the destination file.
|
||||
* @return true on success, false otherwise
|
||||
*/
|
||||
bool DecryptAndWriteFile(const std::string& source, const std::string& destination) const;
|
||||
bool DecryptAndWriteFile(const std::string& source, const std::string& destination,
|
||||
const QuickDecryptor::ProgressCallback& callback = [](std::size_t,
|
||||
std::size_t) {});
|
||||
|
||||
void Abort();
|
||||
|
||||
/**
|
||||
* Decrypts a file and reads it into a vector.
|
||||
@@ -36,6 +43,7 @@ public:
|
||||
|
||||
private:
|
||||
std::string root_folder;
|
||||
QuickDecryptor quick_decryptor;
|
||||
};
|
||||
|
||||
} // namespace Core
|
||||
|
||||
+86
-12
@@ -53,29 +53,34 @@ bool SDMCImporter::IsGood() const {
|
||||
return is_good;
|
||||
}
|
||||
|
||||
bool SDMCImporter::ImportContent(const ContentSpecifier& specifier) {
|
||||
void SDMCImporter::Abort() {
|
||||
decryptor->Abort();
|
||||
}
|
||||
|
||||
bool SDMCImporter::ImportContent(const ContentSpecifier& specifier,
|
||||
const ProgressCallback& callback) {
|
||||
switch (specifier.type) {
|
||||
case ContentType::Application:
|
||||
case ContentType::Update:
|
||||
case ContentType::DLC:
|
||||
return ImportTitle(specifier.id);
|
||||
return ImportTitle(specifier.id, callback);
|
||||
case ContentType::Savegame:
|
||||
return ImportSavegame(specifier.id);
|
||||
return ImportSavegame(specifier.id, callback);
|
||||
case ContentType::Extdata:
|
||||
return ImportExtdata(specifier.id);
|
||||
return ImportExtdata(specifier.id, callback);
|
||||
case ContentType::Sysdata:
|
||||
return ImportSysdata(specifier.id);
|
||||
return ImportSysdata(specifier.id, callback);
|
||||
default:
|
||||
UNREACHABLE();
|
||||
}
|
||||
}
|
||||
|
||||
bool SDMCImporter::ImportTitle(u64 id) {
|
||||
bool SDMCImporter::ImportTitle(u64 id, const ProgressCallback& callback) {
|
||||
const auto path = fmt::format("title/{:08x}/{:08x}/content/", (id >> 32), (id & 0xFFFFFFFF));
|
||||
return FileUtil::ForeachDirectoryEntry(
|
||||
nullptr, config.sdmc_path + path,
|
||||
[this, &path](u64* /*num_entries_out*/, const std::string& directory,
|
||||
const std::string& virtual_name) {
|
||||
[this, &path, callback](u64* /*num_entries_out*/, const std::string& directory,
|
||||
const std::string& virtual_name) {
|
||||
if (FileUtil::IsDirectory(directory + virtual_name)) {
|
||||
return true;
|
||||
}
|
||||
@@ -84,11 +89,12 @@ bool SDMCImporter::ImportTitle(u64 id) {
|
||||
FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) +
|
||||
"Nintendo "
|
||||
"3DS/00000000000000000000000000000000/00000000000000000000000000000000/" +
|
||||
path + virtual_name);
|
||||
path + virtual_name,
|
||||
callback);
|
||||
});
|
||||
}
|
||||
|
||||
bool SDMCImporter::ImportSavegame(u64 id) {
|
||||
bool SDMCImporter::ImportSavegame(u64 id, [[maybe_unused]] const ProgressCallback& callback) {
|
||||
const auto path = fmt::format("title/{:08x}/{:08x}/data/", (id >> 32), (id & 0xFFFFFFFF));
|
||||
|
||||
DataContainer container(decryptor->DecryptFile(fmt::format("/{}00000001.sav", path)));
|
||||
@@ -106,7 +112,7 @@ bool SDMCImporter::ImportSavegame(u64 id) {
|
||||
"Nintendo 3DS/00000000000000000000000000000000/00000000000000000000000000000000/" + path);
|
||||
}
|
||||
|
||||
bool SDMCImporter::ImportExtdata(u64 id) {
|
||||
bool SDMCImporter::ImportExtdata(u64 id, [[maybe_unused]] const ProgressCallback& callback) {
|
||||
const auto path = fmt::format("extdata/{:08x}/{:08x}/", (id >> 32), (id & 0xFFFFFFFF));
|
||||
SDExtdata extdata("/" + path, *decryptor);
|
||||
if (!extdata.IsGood()) {
|
||||
@@ -118,7 +124,7 @@ bool SDMCImporter::ImportExtdata(u64 id) {
|
||||
"Nintendo 3DS/00000000000000000000000000000000/00000000000000000000000000000000/" + path);
|
||||
}
|
||||
|
||||
bool SDMCImporter::ImportSysdata(u64 id) {
|
||||
bool SDMCImporter::ImportSysdata(u64 id, [[maybe_unused]] const ProgressCallback& callback) {
|
||||
switch (id) {
|
||||
case 0: { // boot9.bin
|
||||
const auto target_path = FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + BOOTROM9;
|
||||
@@ -330,6 +336,74 @@ void SDMCImporter::ListSysdata(std::vector<ContentSpecifier>& out) const {
|
||||
} while (0);
|
||||
}
|
||||
|
||||
void SDMCImporter::DeleteContent(const ContentSpecifier& specifier) {
|
||||
switch (specifier.type) {
|
||||
case ContentType::Application:
|
||||
case ContentType::Update:
|
||||
case ContentType::DLC:
|
||||
return DeleteTitle(specifier.id);
|
||||
case ContentType::Savegame:
|
||||
return DeleteSavegame(specifier.id);
|
||||
case ContentType::Extdata:
|
||||
return DeleteExtdata(specifier.id);
|
||||
case ContentType::Sysdata:
|
||||
return DeleteSysdata(specifier.id);
|
||||
default:
|
||||
UNREACHABLE();
|
||||
}
|
||||
}
|
||||
|
||||
void SDMCImporter::DeleteTitle(u64 id) const {
|
||||
FileUtil::DeleteDirRecursively(fmt::format(
|
||||
"{}Nintendo "
|
||||
"3DS/00000000000000000000000000000000/00000000000000000000000000000000/title/{:08x}/{:08x}/"
|
||||
"content/",
|
||||
FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir), (id >> 32), (id & 0xFFFFFFFF)));
|
||||
}
|
||||
|
||||
void SDMCImporter::DeleteSavegame(u64 id) const {
|
||||
FileUtil::DeleteDirRecursively(fmt::format(
|
||||
"{}Nintendo "
|
||||
"3DS/00000000000000000000000000000000/00000000000000000000000000000000/title/{:08x}/{:08x}/"
|
||||
"data/",
|
||||
FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir), (id >> 32), (id & 0xFFFFFFFF)));
|
||||
}
|
||||
|
||||
void SDMCImporter::DeleteExtdata(u64 id) const {
|
||||
FileUtil::DeleteDirRecursively(fmt::format(
|
||||
"{}Nintendo "
|
||||
"3DS/00000000000000000000000000000000/00000000000000000000000000000000/extdata/{:08x}/"
|
||||
"{:08x}/",
|
||||
FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir), (id >> 32), (id & 0xFFFFFFFF)));
|
||||
}
|
||||
|
||||
void SDMCImporter::DeleteSysdata(u64 id) const {
|
||||
switch (id) {
|
||||
case 0: { // boot9.bin
|
||||
FileUtil::Delete(FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + BOOTROM9);
|
||||
}
|
||||
case 1: { // safe mode firm
|
||||
const bool is_new_3ds = FileUtil::Exists(config.safe_mode_firm_path + "new/");
|
||||
const auto target_path =
|
||||
fmt::format("{}00000000000000000000000000000000/title/00040138/{}/",
|
||||
FileUtil::GetUserPath(FileUtil::UserPath::NANDDir),
|
||||
(is_new_3ds ? "20000003" : "00000003"));
|
||||
FileUtil::DeleteDirRecursively(target_path);
|
||||
}
|
||||
case 2: { // seed db
|
||||
FileUtil::Delete(FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + SEED_DB);
|
||||
}
|
||||
case 3: { // secret sector
|
||||
FileUtil::Delete(FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + SECRET_SECTOR);
|
||||
}
|
||||
case 4: { // aes_keys.txt
|
||||
FileUtil::Delete(FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + AES_KEYS);
|
||||
}
|
||||
default:
|
||||
UNREACHABLE_MSG("Unexpected sysdata id {}", id);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Config> LoadPresetConfig(std::string mount_point) {
|
||||
if (mount_point.back() != '/' && mount_point.back() != '\\') {
|
||||
mount_point += '/';
|
||||
|
||||
+26
-6
@@ -61,6 +61,9 @@ struct Config {
|
||||
|
||||
class SDMCImporter {
|
||||
public:
|
||||
/// (current_size, total_size)
|
||||
using ProgressCallback = std::function<void(std::size_t, std::size_t)>;
|
||||
|
||||
/**
|
||||
* Initializes the importer.
|
||||
* @param root_folder Path to the "Nintendo 3DS/<ID0>/<ID1>" folder.
|
||||
@@ -70,10 +73,23 @@ public:
|
||||
~SDMCImporter();
|
||||
|
||||
/**
|
||||
* Dumps a specific content by its specifier.
|
||||
* Aborts 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);
|
||||
bool ImportContent(const ContentSpecifier& specifier,
|
||||
const ProgressCallback& callback = [](std::size_t, std::size_t) {});
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@@ -87,13 +103,17 @@ public:
|
||||
|
||||
private:
|
||||
bool Init();
|
||||
bool ImportTitle(u64 id);
|
||||
bool ImportSavegame(u64 id);
|
||||
bool ImportExtdata(u64 id);
|
||||
bool ImportSysdata(u64 id);
|
||||
bool ImportTitle(u64 id, const ProgressCallback& callback);
|
||||
bool ImportSavegame(u64 id, const ProgressCallback& callback);
|
||||
bool ImportExtdata(u64 id, const ProgressCallback& callback);
|
||||
bool ImportSysdata(u64 id, const ProgressCallback& callback);
|
||||
void ListTitle(std::vector<ContentSpecifier>& out) const;
|
||||
void ListExtdata(std::vector<ContentSpecifier>& out) const;
|
||||
void ListSysdata(std::vector<ContentSpecifier>& out) const;
|
||||
void DeleteTitle(u64 id) const;
|
||||
void DeleteSavegame(u64 id) const;
|
||||
void DeleteExtdata(u64 id) const;
|
||||
void DeleteSysdata(u64 id) const;
|
||||
|
||||
bool is_good{};
|
||||
Config config;
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
// Copyright 2019 threeSD Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <vector>
|
||||
#include <cryptopp/aes.h>
|
||||
#include <cryptopp/files.h>
|
||||
#include <cryptopp/filters.h>
|
||||
#include <cryptopp/modes.h>
|
||||
#include <cryptopp/sha.h>
|
||||
#include "common/assert.h"
|
||||
#include "common/file_util.h"
|
||||
#include "common/string_util.h"
|
||||
#include "core/key/key.h"
|
||||
#include "core/quick_decryptor.h"
|
||||
|
||||
namespace Core {
|
||||
|
||||
QuickDecryptor::QuickDecryptor(const std::string& root_folder_) : root_folder(root_folder_) {
|
||||
ASSERT_MSG(Key::IsNormalKeyAvailable(Key::SDKey),
|
||||
"SD Key must be available in order to decrypt");
|
||||
|
||||
if (root_folder.back() == '/' || root_folder.back() == '\\') {
|
||||
// Remove '/' or '\' character at the end as we will add them back when combining path
|
||||
root_folder.erase(root_folder.size() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
QuickDecryptor::~QuickDecryptor() = default;
|
||||
|
||||
namespace {
|
||||
std::array<u8, 16> GetFileCTR(const std::string& path) {
|
||||
auto path_utf16 = Common::UTF8ToUTF16(path);
|
||||
std::vector<u8> path_data(path_utf16.size() * 2 + 2, 0); // Add the '\0' character
|
||||
std::memcpy(path_data.data(), path_utf16.data(), path_utf16.size() * 2);
|
||||
|
||||
CryptoPP::SHA256 sha;
|
||||
std::array<u8, CryptoPP::SHA256::DIGESTSIZE> hash;
|
||||
sha.CalculateDigest(hash.data(), path_data.data(), path_data.size());
|
||||
|
||||
std::array<u8, 16> ctr;
|
||||
for (int i = 0; i < 16; i++) {
|
||||
ctr[i] = hash[i] ^ hash[16 + i];
|
||||
}
|
||||
return ctr;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool QuickDecryptor::DecryptAndWriteFile(const std::string& source_,
|
||||
const std::string& destination_,
|
||||
const ProgressCallback& callback_) {
|
||||
if (is_running) {
|
||||
LOG_ERROR(Core, "Decryptor is running");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!FileUtil::CreateFullPath(destination_)) {
|
||||
LOG_ERROR(Core, "Could not create path {}", destination_);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (auto& event : data_read_event) {
|
||||
event.Reset();
|
||||
}
|
||||
for (auto& event : data_decrypted_event) {
|
||||
event.Reset();
|
||||
}
|
||||
for (auto& event : data_written_event) {
|
||||
event.Reset();
|
||||
}
|
||||
completion_event.Reset();
|
||||
|
||||
source = source_;
|
||||
destination = destination_;
|
||||
callback = callback_;
|
||||
|
||||
total_size = FileUtil::GetSize(root_folder + source);
|
||||
if (total_size == 0) {
|
||||
LOG_ERROR(Core, "Could not open file {}", root_folder + source);
|
||||
return false;
|
||||
}
|
||||
|
||||
is_good = is_running = true;
|
||||
|
||||
read_thread = std::make_unique<std::thread>(&QuickDecryptor::DataReadLoop, this);
|
||||
decrypt_thread = std::make_unique<std::thread>(&QuickDecryptor::DataDecryptLoop, this);
|
||||
write_thread = std::make_unique<std::thread>(&QuickDecryptor::DataWriteLoop, this);
|
||||
|
||||
completion_event.Wait();
|
||||
is_running = false;
|
||||
|
||||
read_thread->join();
|
||||
decrypt_thread->join();
|
||||
write_thread->join();
|
||||
|
||||
bool ret = is_good;
|
||||
is_good = true;
|
||||
return ret;
|
||||
}
|
||||
|
||||
void QuickDecryptor::DataReadLoop() {
|
||||
std::size_t current_buffer = 0;
|
||||
bool is_first_run = true;
|
||||
|
||||
FileUtil::IOFile file(root_folder + source, "rb");
|
||||
if (!file) {
|
||||
is_good = false;
|
||||
completion_event.Set();
|
||||
return;
|
||||
}
|
||||
|
||||
std::size_t file_size = total_size;
|
||||
|
||||
while (is_running && file_size > 0) {
|
||||
if (is_first_run) {
|
||||
if (current_buffer == buffers.size() - 1) {
|
||||
is_first_run = false;
|
||||
}
|
||||
} else {
|
||||
data_written_event[current_buffer].Wait();
|
||||
}
|
||||
|
||||
const auto bytes_to_read = std::min(BufferSize, file_size);
|
||||
if (file.ReadBytes(buffers[current_buffer].data(), bytes_to_read) != bytes_to_read) {
|
||||
is_good = false;
|
||||
completion_event.Set();
|
||||
return;
|
||||
}
|
||||
file_size -= bytes_to_read;
|
||||
|
||||
data_read_event[current_buffer].Set();
|
||||
current_buffer = (current_buffer + 1) % buffers.size();
|
||||
}
|
||||
}
|
||||
|
||||
void QuickDecryptor::DataDecryptLoop() {
|
||||
auto ctr = GetFileCTR(source);
|
||||
auto key = Key::GetNormalKey(Key::SDKey);
|
||||
CryptoPP::CTR_Mode<CryptoPP::AES>::Decryption aes;
|
||||
aes.SetKeyWithIV(key.data(), key.size(), ctr.data());
|
||||
|
||||
std::size_t current_buffer = 0;
|
||||
std::size_t file_size = total_size;
|
||||
|
||||
while (is_running && file_size > 0) {
|
||||
data_read_event[current_buffer].Wait();
|
||||
|
||||
const auto bytes_to_process = std::min(BufferSize, file_size);
|
||||
aes.ProcessData(buffers[current_buffer].data(), buffers[current_buffer].data(),
|
||||
bytes_to_process);
|
||||
|
||||
file_size -= bytes_to_process;
|
||||
|
||||
data_decrypted_event[current_buffer].Set();
|
||||
current_buffer = (current_buffer + 1) % buffers.size();
|
||||
}
|
||||
}
|
||||
|
||||
void QuickDecryptor::DataWriteLoop() {
|
||||
std::size_t current_buffer = 0;
|
||||
|
||||
FileUtil::IOFile file(destination, "wb");
|
||||
if (!file) {
|
||||
is_good = false;
|
||||
completion_event.Set();
|
||||
return;
|
||||
}
|
||||
|
||||
std::size_t file_size = total_size;
|
||||
std::size_t iteration = 0;
|
||||
/// The number of iterations each progress report covers. 32 * 16K = 512K
|
||||
constexpr std::size_t ProgressReportFreq = 32;
|
||||
|
||||
while (is_running && file_size > 0) {
|
||||
iteration++;
|
||||
if (iteration % ProgressReportFreq == 0) {
|
||||
callback(iteration * BufferSize, total_size);
|
||||
}
|
||||
|
||||
data_decrypted_event[current_buffer].Wait();
|
||||
|
||||
const auto bytes_to_write = std::min(BufferSize, file_size);
|
||||
if (file.WriteBytes(buffers[current_buffer].data(), bytes_to_write) != bytes_to_write) {
|
||||
is_good = false;
|
||||
completion_event.Set();
|
||||
return;
|
||||
}
|
||||
file_size -= bytes_to_write;
|
||||
|
||||
data_written_event[current_buffer].Set();
|
||||
current_buffer = (current_buffer + 1) % buffers.size();
|
||||
}
|
||||
|
||||
completion_event.Set();
|
||||
}
|
||||
|
||||
void QuickDecryptor::Abort() {
|
||||
if (is_running.exchange(false)) {
|
||||
is_good = false;
|
||||
completion_event.Set();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Core
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright 2019 threeSD Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include "common/common_types.h"
|
||||
#include "common/thread.h"
|
||||
|
||||
namespace Core {
|
||||
|
||||
class QuickDecryptor {
|
||||
public:
|
||||
/// (current_size, total_size)
|
||||
using ProgressCallback = std::function<void(std::size_t, std::size_t)>;
|
||||
|
||||
/**
|
||||
* Initializes the decryptor.
|
||||
* @param root_folder Path to the "Nintendo 3DS/<ID0>/<ID1>" folder.
|
||||
*/
|
||||
explicit QuickDecryptor(const std::string& root_folder);
|
||||
|
||||
~QuickDecryptor();
|
||||
|
||||
bool DecryptAndWriteFile(const std::string& source, const std::string& destination,
|
||||
const ProgressCallback& callback = [](std::size_t, std::size_t) {});
|
||||
|
||||
void DataReadLoop();
|
||||
void DataDecryptLoop();
|
||||
void DataWriteLoop();
|
||||
|
||||
void Abort();
|
||||
|
||||
private:
|
||||
static constexpr std::size_t BufferSize = 16 * 1024; // 16 KB
|
||||
|
||||
std::string root_folder;
|
||||
std::string source;
|
||||
std::string destination;
|
||||
std::size_t total_size{};
|
||||
|
||||
std::array<std::array<u8, BufferSize>, 3> buffers;
|
||||
std::array<Common::Event, 3> data_read_event;
|
||||
std::array<Common::Event, 3> data_decrypted_event;
|
||||
std::array<Common::Event, 3> data_written_event;
|
||||
|
||||
std::unique_ptr<std::thread> read_thread;
|
||||
std::unique_ptr<std::thread> decrypt_thread;
|
||||
std::unique_ptr<std::thread> write_thread;
|
||||
|
||||
ProgressCallback callback;
|
||||
|
||||
Common::Event completion_event;
|
||||
bool is_good{true};
|
||||
std::atomic_bool is_running{false};
|
||||
};
|
||||
|
||||
} // namespace Core
|
||||
@@ -266,18 +266,32 @@ void ImportDialog::StartImporting() {
|
||||
auto* dialog = new QProgressDialog(tr("Initializing..."), tr("Cancel"), 0,
|
||||
static_cast<int>(total_size / multiplier), this);
|
||||
dialog->setWindowModality(Qt::WindowModal);
|
||||
dialog->setMinimumDuration(0);
|
||||
dialog->setValue(0);
|
||||
|
||||
auto* job = new ImportJob(this, importer, std::move(to_import));
|
||||
|
||||
connect(job, &ImportJob::ProgressUpdated, this,
|
||||
[dialog, multiplier, total_count](u64 size_imported, u64 count,
|
||||
Core::ContentSpecifier next_content) {
|
||||
connect(job, &ImportJob::NextContent, this,
|
||||
[this, dialog, multiplier, total_count](u64 size_imported, u64 count,
|
||||
Core::ContentSpecifier next_content) {
|
||||
dialog->setValue(static_cast<int>(size_imported / multiplier));
|
||||
dialog->setLabelText(tr("Importing %1 (%2/%3)...")
|
||||
.arg(GetContentName(next_content))
|
||||
dialog->setLabelText(tr("(%1/%2) Importing %3...")
|
||||
.arg(count)
|
||||
.arg(total_count));
|
||||
.arg(total_count)
|
||||
.arg(GetContentName(next_content)));
|
||||
current_content = next_content;
|
||||
current_count = count;
|
||||
});
|
||||
connect(job, &ImportJob::ProgressUpdated, this,
|
||||
[this, dialog, multiplier, total_count](u64 total_size_imported,
|
||||
u64 current_size_imported) {
|
||||
dialog->setValue(static_cast<int>(total_size_imported / multiplier));
|
||||
dialog->setLabelText(tr("(%1/%2) Importing %3 (%4/%5)...")
|
||||
.arg(current_count)
|
||||
.arg(total_count)
|
||||
.arg(GetContentName(current_content))
|
||||
.arg(ReadableByteSize(current_size_imported))
|
||||
.arg(ReadableByteSize(current_content.maximum_size)));
|
||||
});
|
||||
connect(job, &ImportJob::ErrorOccured, this,
|
||||
[this, dialog](Core::ContentSpecifier current_content) {
|
||||
|
||||
@@ -41,4 +41,8 @@ private:
|
||||
// HACK: To tell whether the checkbox state change is a programmatic trigger
|
||||
// TODO: Is there a more elegant way of doing the same?
|
||||
bool program_trigger = false;
|
||||
|
||||
// TODO: Why this won't work as locals?
|
||||
Core::ContentSpecifier current_content = {};
|
||||
u64 current_count = 0;
|
||||
};
|
||||
|
||||
@@ -15,10 +15,17 @@ ImportJob::~ImportJob() = default;
|
||||
void ImportJob::run() {
|
||||
u64 size_imported = 0, count = 0;
|
||||
for (const auto& content : contents) {
|
||||
emit ProgressUpdated(size_imported, count + 1, content);
|
||||
if (!importer.ImportContent(content)) {
|
||||
emit ErrorOccured(content);
|
||||
return;
|
||||
emit NextContent(size_imported, count + 1, content);
|
||||
const auto callback = [this, size_imported](std::size_t current_size,
|
||||
std::size_t /*total_size*/) {
|
||||
emit ProgressUpdated(size_imported + current_size, current_size);
|
||||
};
|
||||
if (!importer.ImportContent(content, callback)) {
|
||||
importer.DeleteContent(content);
|
||||
if (!cancelled) {
|
||||
emit ErrorOccured(content);
|
||||
return;
|
||||
}
|
||||
}
|
||||
count++;
|
||||
size_imported += content.maximum_size;
|
||||
@@ -32,4 +39,5 @@ void ImportJob::run() {
|
||||
|
||||
void ImportJob::Cancel() {
|
||||
cancelled.store(true);
|
||||
importer.Abort();
|
||||
}
|
||||
|
||||
@@ -20,7 +20,17 @@ public:
|
||||
void Cancel();
|
||||
|
||||
signals:
|
||||
void ProgressUpdated(u64 size_imported, u64 count, Core::ContentSpecifier next_content);
|
||||
/**
|
||||
* Called when progress is updated on the current content.
|
||||
* @param total_size_imported Total imported size taking all previous contents into
|
||||
* consideration.
|
||||
* @param current_size_imported Imported size of the current content.
|
||||
*/
|
||||
void ProgressUpdated(u64 total_size_imported, u64 current_size_imported);
|
||||
|
||||
/// Dumping of a content has been finished, go on to the next. Called at start as well.
|
||||
void NextContent(u64 size_imported, u64 count, Core::ContentSpecifier next_content);
|
||||
|
||||
void Completed();
|
||||
void ErrorOccured(Core::ContentSpecifier current_content);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user