mirror of
https://github.com/Dark98/threeSD.git
synced 2026-07-07 16:59:07 +00:00
Fixes to CIA building
1. Optimizes ticket finding 2. Fixes progress reporting overshoot 3. Use u64 for size in general 4. Various other fixes and cleanups
This commit is contained in:
@@ -10,6 +10,7 @@ add_library(common STATIC
|
|||||||
logging/log.cpp
|
logging/log.cpp
|
||||||
logging/log.h
|
logging/log.h
|
||||||
misc.cpp
|
misc.cpp
|
||||||
|
progress_callback.cpp
|
||||||
progress_callback.h
|
progress_callback.h
|
||||||
scope_exit.h
|
scope_exit.h
|
||||||
string_util.cpp
|
string_util.cpp
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// Copyright 2021 threeSD Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include "common/progress_callback.h"
|
||||||
|
|
||||||
|
namespace Common {
|
||||||
|
|
||||||
|
ProgressCallback ProgressCallbackWrapper::Wrap(const ProgressCallback& callback) {
|
||||||
|
current_done_size += current_pending_size; // Last content finished
|
||||||
|
return [this, callback](std::size_t current, std::size_t total) {
|
||||||
|
current_pending_size = total;
|
||||||
|
callback(current + current_done_size, total_size);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ProgressCallback ProgressCallbackWrapper::Wrap(
|
||||||
|
const std::function<void(std::size_t, std::size_t, std::size_t)>& callback) {
|
||||||
|
|
||||||
|
current_done_size += current_pending_size; // Last content finished
|
||||||
|
return [this, callback](std::size_t current, std::size_t total) {
|
||||||
|
current_pending_size = total;
|
||||||
|
callback(current, current + current_done_size, total_size);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgressCallbackWrapper::SetCurrent(u64 current) {
|
||||||
|
current_done_size = current;
|
||||||
|
current_pending_size = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Common
|
||||||
@@ -5,10 +5,26 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include "common/common_types.h"
|
||||||
|
|
||||||
namespace Common {
|
namespace Common {
|
||||||
|
|
||||||
// (current_size, total_size)
|
// (current_size, total_size)
|
||||||
using ProgressCallback = std::function<void(std::size_t, std::size_t)>;
|
using ProgressCallback = std::function<void(u64, u64)>;
|
||||||
|
|
||||||
|
class ProgressCallbackWrapper {
|
||||||
|
public:
|
||||||
|
// (total imported size, total size)
|
||||||
|
ProgressCallback Wrap(const ProgressCallback& callback);
|
||||||
|
|
||||||
|
// (current content imported size, total imported size, total size)
|
||||||
|
ProgressCallback Wrap(const std::function<void(u64, u64, u64)>& callback);
|
||||||
|
|
||||||
|
void SetCurrent(u64 current);
|
||||||
|
|
||||||
|
std::size_t total_size{};
|
||||||
|
std::size_t current_done_size{};
|
||||||
|
std::size_t current_pending_size{};
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace Common
|
} // namespace Common
|
||||||
|
|||||||
+45
-32
@@ -53,12 +53,28 @@ private:
|
|||||||
bool hash_enabled{};
|
bool hash_enabled{};
|
||||||
};
|
};
|
||||||
|
|
||||||
CIABuilder::CIABuilder() = default;
|
CIABuilder::CIABuilder(const Config& config) {
|
||||||
|
if (!config.ticket_db_path.empty()) {
|
||||||
|
ticket_db = std::make_unique<TicketDB>(config.ticket_db_path);
|
||||||
|
}
|
||||||
|
if (!ticket_db || !ticket_db->IsGood()) {
|
||||||
|
LOG_WARNING(Core, "ticket.db not present or is invalid");
|
||||||
|
ticket_db.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.enc_title_keys_bin_path.empty()) {
|
||||||
|
enc_title_keys_bin = std::make_unique<EncTitleKeysBin>();
|
||||||
|
if (!LoadTitleKeysBin(*enc_title_keys_bin, config.enc_title_keys_bin_path)) {
|
||||||
|
LOG_WARNING(Core, "encTitleKeys.bin invalid");
|
||||||
|
enc_title_keys_bin.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CIABuilder::~CIABuilder() = default;
|
CIABuilder::~CIABuilder() = default;
|
||||||
|
|
||||||
bool CIABuilder::Init(CIABuildType type_, const std::string& destination, TitleMetadata tmd_,
|
bool CIABuilder::Init(CIABuildType type_, const std::string& destination, TitleMetadata tmd_,
|
||||||
const Config& config, std::size_t total_size_,
|
std::size_t total_size_, const Common::ProgressCallback& callback_) {
|
||||||
const Common::ProgressCallback& callback_) {
|
|
||||||
|
|
||||||
type = type_;
|
type = type_;
|
||||||
header = {};
|
header = {};
|
||||||
@@ -91,14 +107,14 @@ bool CIABuilder::Init(CIABuildType type_, const std::string& destination, TitleM
|
|||||||
// Cert
|
// Cert
|
||||||
cert_offset = Common::AlignUp(header.header_size, CIA_ALIGNMENT);
|
cert_offset = Common::AlignUp(header.header_size, CIA_ALIGNMENT);
|
||||||
header.cert_size = CIA_CERT_SIZE;
|
header.cert_size = CIA_CERT_SIZE;
|
||||||
if (!WriteCert(config.certs_db_path)) {
|
if (!WriteCert()) {
|
||||||
LOG_ERROR(Core, "Could not write cert to file {}", destination);
|
LOG_ERROR(Core, "Could not write cert to file {}", destination);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ticket
|
// Ticket
|
||||||
ticket_offset = Common::AlignUp(cert_offset + header.cert_size, CIA_ALIGNMENT);
|
ticket_offset = Common::AlignUp(cert_offset + header.cert_size, CIA_ALIGNMENT);
|
||||||
if (!WriteTicket(config.ticket_db_path, config.enc_title_keys_bin_path)) {
|
if (!WriteTicket()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,15 +128,23 @@ bool CIABuilder::Init(CIABuildType type_, const std::string& destination, TitleM
|
|||||||
// Meta will be written in Finalize
|
// Meta will be written in Finalize
|
||||||
header.meta_size = 0;
|
header.meta_size = 0;
|
||||||
|
|
||||||
|
// Initialize variables
|
||||||
written = content_offset;
|
written = content_offset;
|
||||||
total_size = total_size_;
|
total_size = total_size_;
|
||||||
|
|
||||||
callback = callback_;
|
callback = callback_;
|
||||||
|
wrapper.total_size = total_size;
|
||||||
|
wrapper.SetCurrent(written);
|
||||||
|
|
||||||
callback(written, total_size);
|
callback(written, total_size);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CIABuilder::WriteCert(const std::string& certs_db_path) {
|
void CIABuilder::Cleanup() {
|
||||||
|
file.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CIABuilder::WriteCert() {
|
||||||
if (!Certs::IsLoaded()) {
|
if (!Certs::IsLoaded()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -135,10 +159,9 @@ bool CIABuilder::WriteCert(const std::string& certs_db_path) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool FindLegitTicket(Ticket& ticket, u64 title_id, const std::string& ticket_db_path) {
|
bool CIABuilder::FindLegitTicket(Ticket& ticket, u64 title_id) const {
|
||||||
TicketDB ticket_db(ticket_db_path);
|
if (ticket_db && ticket_db->tickets.count(title_id)) {
|
||||||
if (ticket_db.IsGood() && ticket_db.tickets.count(title_id)) {
|
ticket = ticket_db->tickets.at(title_id);
|
||||||
ticket = ticket_db.tickets.at(title_id);
|
|
||||||
if (!ticket.ValidateSignature()) {
|
if (!ticket.ValidateSignature()) {
|
||||||
LOG_ERROR(Core, "Ticket in ticket.db for {:016x} is not legit", title_id);
|
LOG_ERROR(Core, "Ticket in ticket.db for {:016x} is not legit", title_id);
|
||||||
return false;
|
return false;
|
||||||
@@ -150,24 +173,17 @@ static bool FindLegitTicket(Ticket& ticket, u64 title_id, const std::string& tic
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Ticket BuildStandardTicket(u64 title_id, const std::string& ticket_db_path,
|
Ticket CIABuilder::BuildStandardTicket(u64 title_id) const {
|
||||||
const std::string& enc_title_keys_bin_path) {
|
|
||||||
Ticket ticket = BuildFakeTicket(title_id);
|
Ticket ticket = BuildFakeTicket(title_id);
|
||||||
|
|
||||||
// Fill in common_key_index and title_key from either ticket.db (installed tickets)
|
// Fill in common_key_index and title_key from either ticket.db (installed tickets)
|
||||||
// or GM9 support files (encTitleKeys.bin) found on the SD card
|
// or GM9 support files (encTitleKeys.bin) found on the SD card
|
||||||
if (TicketDB ticket_db(ticket_db_path);
|
if (ticket_db && ticket_db->tickets.count(title_id)) { // ticket.db
|
||||||
ticket_db.IsGood() && ticket_db.tickets.count(title_id)) { // ticket.db
|
const auto& legit_ticket = ticket_db->tickets.at(title_id);
|
||||||
|
|
||||||
const auto& legit_ticket = ticket_db.tickets.at(title_id);
|
|
||||||
ticket.body.common_key_index = legit_ticket.body.common_key_index;
|
ticket.body.common_key_index = legit_ticket.body.common_key_index;
|
||||||
ticket.body.title_key = legit_ticket.body.title_key;
|
ticket.body.title_key = legit_ticket.body.title_key;
|
||||||
|
} else if (enc_title_keys_bin && enc_title_keys_bin->count(title_id)) { // support files
|
||||||
} else if (TitleKeysMap enc_title_keys;
|
const auto& entry = enc_title_keys_bin->at(title_id);
|
||||||
LoadTitleKeysBin(enc_title_keys, enc_title_keys_bin_path) &&
|
|
||||||
enc_title_keys.count(title_id)) { // support files
|
|
||||||
|
|
||||||
const auto& entry = enc_title_keys.at(title_id);
|
|
||||||
ticket.body.common_key_index = entry.common_key_index;
|
ticket.body.common_key_index = entry.common_key_index;
|
||||||
ticket.body.title_key = entry.title_key;
|
ticket.body.title_key = entry.title_key;
|
||||||
} else {
|
} else {
|
||||||
@@ -195,18 +211,16 @@ static Key::AESKey GetTitleKey(const Ticket& ticket) {
|
|||||||
return title_key;
|
return title_key;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CIABuilder::WriteTicket(const std::string& ticket_db_path,
|
bool CIABuilder::WriteTicket() {
|
||||||
const std::string& enc_title_keys_bin_path) {
|
|
||||||
|
|
||||||
const auto title_id = tmd.GetTitleID();
|
const auto title_id = tmd.GetTitleID();
|
||||||
|
|
||||||
Ticket ticket;
|
Ticket ticket;
|
||||||
if (type == CIABuildType::Legit) {
|
if (type == CIABuildType::Legit) {
|
||||||
if (!FindLegitTicket(ticket, title_id, ticket_db_path)) {
|
if (!FindLegitTicket(ticket, title_id)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ticket = BuildStandardTicket(title_id, ticket_db_path, enc_title_keys_bin_path);
|
ticket = BuildStandardTicket(title_id);
|
||||||
}
|
}
|
||||||
title_key = GetTitleKey(ticket);
|
title_key = GetTitleKey(ticket);
|
||||||
|
|
||||||
@@ -248,11 +262,10 @@ bool CIABuilder::AddContent(u16 content_id, NCCHContainer& ncch) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
file->Seek(written, SEEK_SET); // To enforce alignment
|
file->Seek(written, SEEK_SET); // To enforce alignment
|
||||||
|
wrapper.SetCurrent(written);
|
||||||
|
|
||||||
auto& tmd_chunk = tmd.GetContentChunkByID(content_id);
|
auto& tmd_chunk = tmd.GetContentChunkByID(content_id);
|
||||||
const auto progress_callback = [this](std::size_t current, std::size_t total) {
|
|
||||||
callback(written + current, total_size);
|
|
||||||
};
|
|
||||||
if (type == CIABuildType::Standard) {
|
if (type == CIABuildType::Standard) {
|
||||||
// Decrypt the NCCH. We created a HashedFile to transparently calculate the hash as there
|
// Decrypt the NCCH. We created a HashedFile to transparently calculate the hash as there
|
||||||
// is no easy way to get decrypted NCCH content otherwise.
|
// is no easy way to get decrypted NCCH content otherwise.
|
||||||
@@ -261,7 +274,7 @@ bool CIABuilder::AddContent(u16 content_id, NCCHContainer& ncch) {
|
|||||||
std::lock_guard lock{abort_ncch_mutex};
|
std::lock_guard lock{abort_ncch_mutex};
|
||||||
abort_ncch = &ncch;
|
abort_ncch = &ncch;
|
||||||
}
|
}
|
||||||
const auto ret = ncch.DecryptToFile(file, progress_callback);
|
const auto ret = ncch.DecryptToFile(file, wrapper.Wrap(callback));
|
||||||
{
|
{
|
||||||
std::lock_guard lock{abort_ncch_mutex};
|
std::lock_guard lock{abort_ncch_mutex};
|
||||||
abort_ncch = nullptr;
|
abort_ncch = nullptr;
|
||||||
@@ -292,7 +305,7 @@ bool CIABuilder::AddContent(u16 content_id, NCCHContainer& ncch) {
|
|||||||
}
|
}
|
||||||
decryptor.SetCrypto(crypto);
|
decryptor.SetCrypto(crypto);
|
||||||
if (!decryptor.CryptAndWriteFile(ncch.file, ncch.file->GetSize(), file,
|
if (!decryptor.CryptAndWriteFile(ncch.file, ncch.file->GetSize(), file,
|
||||||
progress_callback)) {
|
wrapper.Wrap(callback))) {
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-5
@@ -25,11 +25,14 @@ constexpr std::size_t CIA_CERT_SIZE = 0xA00;
|
|||||||
constexpr std::size_t CIA_METADATA_SIZE = 0x3AC0;
|
constexpr std::size_t CIA_METADATA_SIZE = 0x3AC0;
|
||||||
|
|
||||||
struct Config;
|
struct Config;
|
||||||
|
class EncTitleKeysBin;
|
||||||
class HashedFile;
|
class HashedFile;
|
||||||
|
class Ticket;
|
||||||
|
class TicketDB;
|
||||||
|
|
||||||
class CIABuilder {
|
class CIABuilder {
|
||||||
public:
|
public:
|
||||||
explicit CIABuilder();
|
explicit CIABuilder(const Config& config);
|
||||||
~CIABuilder();
|
~CIABuilder();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,8 +40,9 @@ public:
|
|||||||
* @return true on success, false otherwise
|
* @return true on success, false otherwise
|
||||||
*/
|
*/
|
||||||
bool Init(CIABuildType type, const std::string& destination, TitleMetadata tmd,
|
bool Init(CIABuildType type, const std::string& destination, TitleMetadata tmd,
|
||||||
const Config& config, std::size_t total_size,
|
std::size_t total_size, const Common::ProgressCallback& callback);
|
||||||
const Common::ProgressCallback& callback);
|
|
||||||
|
void Cleanup();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds an NCCH content to the CIA.
|
* Adds an NCCH content to the CIA.
|
||||||
@@ -93,9 +97,17 @@ private:
|
|||||||
|
|
||||||
static_assert(sizeof(Metadata) == CIA_METADATA_SIZE, "CIA Metadata structure size is wrong");
|
static_assert(sizeof(Metadata) == CIA_METADATA_SIZE, "CIA Metadata structure size is wrong");
|
||||||
|
|
||||||
bool WriteCert(const std::string& certs_db_path);
|
bool WriteCert();
|
||||||
bool WriteTicket(const std::string& ticket_db_path, const std::string& enc_title_keys_bin_path);
|
|
||||||
|
|
||||||
|
bool FindLegitTicket(Ticket& ticket, u64 title_id) const;
|
||||||
|
Ticket BuildStandardTicket(u64 title_id) const;
|
||||||
|
bool WriteTicket();
|
||||||
|
|
||||||
|
// Persistent state
|
||||||
|
std::unique_ptr<TicketDB> ticket_db;
|
||||||
|
std::unique_ptr<EncTitleKeysBin> enc_title_keys_bin;
|
||||||
|
|
||||||
|
// State of a single task
|
||||||
CIABuildType type;
|
CIABuildType type;
|
||||||
|
|
||||||
Header header{};
|
Header header{};
|
||||||
@@ -113,6 +125,7 @@ private:
|
|||||||
std::size_t written{}; // size written (with alignment)
|
std::size_t written{}; // size written (with alignment)
|
||||||
std::size_t total_size{};
|
std::size_t total_size{};
|
||||||
Common::ProgressCallback callback;
|
Common::ProgressCallback callback;
|
||||||
|
Common::ProgressCallbackWrapper wrapper;
|
||||||
|
|
||||||
// The NCCH to abort on
|
// The NCCH to abort on
|
||||||
std::mutex abort_ncch_mutex;
|
std::mutex abort_ncch_mutex;
|
||||||
|
|||||||
@@ -116,6 +116,11 @@ void Load(const std::string& path) {
|
|||||||
g_seeddb_loaded = g_seeddb.Load(path);
|
g_seeddb_loaded = g_seeddb.Load(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Clear() {
|
||||||
|
g_seeddb.Clear();
|
||||||
|
g_seeddb_loaded = false;
|
||||||
|
}
|
||||||
|
|
||||||
std::optional<Seed::Data> GetSeed(u64 title_id) {
|
std::optional<Seed::Data> GetSeed(u64 title_id) {
|
||||||
if (!g_seeddb_loaded) {
|
if (!g_seeddb_loaded) {
|
||||||
Load(fmt::format("{}/seeddb.bin", FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir)));
|
Load(fmt::format("{}/seeddb.bin", FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir)));
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ public:
|
|||||||
std::size_t Size() const;
|
std::size_t Size() const;
|
||||||
std::optional<Seed::Data> Get(u64 title_id) const;
|
std::optional<Seed::Data> Get(u64 title_id) const;
|
||||||
|
|
||||||
|
void Clear() {
|
||||||
|
seeds.clear();
|
||||||
|
}
|
||||||
|
|
||||||
auto begin() {
|
auto begin() {
|
||||||
return seeds.begin();
|
return seeds.begin();
|
||||||
}
|
}
|
||||||
@@ -56,6 +60,7 @@ private:
|
|||||||
namespace Seeds {
|
namespace Seeds {
|
||||||
|
|
||||||
void Load(const std::string& path);
|
void Load(const std::string& path);
|
||||||
|
void Clear();
|
||||||
std::optional<Seed::Data> GetSeed(u64 title_id);
|
std::optional<Seed::Data> GetSeed(u64 title_id);
|
||||||
|
|
||||||
} // namespace Seeds
|
} // namespace Seeds
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ struct TitleKeysBinEntry {
|
|||||||
static_assert(sizeof(TitleKeysBinEntry) == 32);
|
static_assert(sizeof(TitleKeysBinEntry) == 32);
|
||||||
|
|
||||||
using TitleKeysMap = std::unordered_map<u64, TitleKeysBinEntry>;
|
using TitleKeysMap = std::unordered_map<u64, TitleKeysBinEntry>;
|
||||||
|
class EncTitleKeysBin : public TitleKeysMap {};
|
||||||
|
|
||||||
// GM9 support files encTitleKeys.bin and decTitleKeys.bin.
|
// GM9 support files encTitleKeys.bin and decTitleKeys.bin.
|
||||||
bool LoadTitleKeysBin(TitleKeysMap& out, const std::string& path);
|
bool LoadTitleKeysBin(TitleKeysMap& out, const std::string& path);
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ bool FileDecryptor::CryptAndWriteFile(std::shared_ptr<FileUtil::IOFile> source_,
|
|||||||
destination = std::move(destination_);
|
destination = std::move(destination_);
|
||||||
callback = callback_;
|
callback = callback_;
|
||||||
|
|
||||||
current_total_size = size;
|
total_size = size;
|
||||||
|
|
||||||
is_good = is_running = true;
|
is_good = is_running = true;
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ void FileDecryptor::DataReadLoop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::size_t file_size = current_total_size;
|
std::size_t file_size = total_size;
|
||||||
|
|
||||||
while (is_running && file_size > 0) {
|
while (is_running && file_size > 0) {
|
||||||
if (is_first_run) {
|
if (is_first_run) {
|
||||||
@@ -114,7 +114,7 @@ void FileDecryptor::DataReadLoop() {
|
|||||||
|
|
||||||
void FileDecryptor::DataDecryptLoop() {
|
void FileDecryptor::DataDecryptLoop() {
|
||||||
std::size_t current_buffer = 0;
|
std::size_t current_buffer = 0;
|
||||||
std::size_t file_size = current_total_size;
|
std::size_t file_size = total_size;
|
||||||
|
|
||||||
while (is_running && file_size > 0) {
|
while (is_running && file_size > 0) {
|
||||||
data_read_event[current_buffer].Wait();
|
data_read_event[current_buffer].Wait();
|
||||||
@@ -138,7 +138,8 @@ void FileDecryptor::DataWriteLoop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::size_t file_size = current_total_size;
|
std::size_t file_size = total_size;
|
||||||
|
std::size_t imported_size = 0;
|
||||||
std::size_t iteration = 0;
|
std::size_t iteration = 0;
|
||||||
/// The number of iterations each progress report covers. 32 * 16K = 512K
|
/// The number of iterations each progress report covers. 32 * 16K = 512K
|
||||||
constexpr std::size_t ProgressReportFreq = 32;
|
constexpr std::size_t ProgressReportFreq = 32;
|
||||||
@@ -180,11 +181,6 @@ void FileDecryptor::Abort() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileDecryptor::Reset(std::size_t total_size_) {
|
|
||||||
total_size = total_size_;
|
|
||||||
imported_size = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
CryptoFunc::~CryptoFunc() = default;
|
CryptoFunc::~CryptoFunc() = default;
|
||||||
|
|
||||||
class CryptoFunc_AES_CTR final : public CryptoFunc {
|
class CryptoFunc_AES_CTR final : public CryptoFunc {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ public:
|
|||||||
bool CryptAndWriteFile(
|
bool CryptAndWriteFile(
|
||||||
std::shared_ptr<FileUtil::IOFile> source, std::size_t size,
|
std::shared_ptr<FileUtil::IOFile> source, std::size_t size,
|
||||||
std::shared_ptr<FileUtil::IOFile> destination,
|
std::shared_ptr<FileUtil::IOFile> destination,
|
||||||
const Common::ProgressCallback& callback = [](std::size_t, std::size_t) {});
|
const Common::ProgressCallback& callback = [](u64, u64) {});
|
||||||
|
|
||||||
void DataReadLoop();
|
void DataReadLoop();
|
||||||
void DataDecryptLoop();
|
void DataDecryptLoop();
|
||||||
@@ -52,9 +52,6 @@ public:
|
|||||||
|
|
||||||
void Abort();
|
void Abort();
|
||||||
|
|
||||||
/// Reset the imported_size counter for this content and set a new total_size.
|
|
||||||
void Reset(std::size_t total_size);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static constexpr std::size_t BufferSize = 16 * 1024; // 16 KB
|
static constexpr std::size_t BufferSize = 16 * 1024; // 16 KB
|
||||||
|
|
||||||
@@ -62,12 +59,7 @@ private:
|
|||||||
std::shared_ptr<FileUtil::IOFile> destination;
|
std::shared_ptr<FileUtil::IOFile> destination;
|
||||||
std::shared_ptr<CryptoFunc> crypto;
|
std::shared_ptr<CryptoFunc> crypto;
|
||||||
|
|
||||||
// Total size of this content, may consist of multiple files
|
|
||||||
std::size_t total_size{};
|
std::size_t total_size{};
|
||||||
// Total size of the current file to process
|
|
||||||
std::size_t current_total_size{};
|
|
||||||
// Total imported size for this content
|
|
||||||
std::size_t imported_size{};
|
|
||||||
|
|
||||||
std::array<std::array<u8, BufferSize>, 3> buffers;
|
std::array<std::array<u8, BufferSize>, 3> buffers;
|
||||||
std::array<Common::Event, 3> data_read_event;
|
std::array<Common::Event, 3> data_read_event;
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ static std::unordered_map<std::string, Certificate> g_certs;
|
|||||||
static bool g_is_loaded = false;
|
static bool g_is_loaded = false;
|
||||||
|
|
||||||
bool Load(const std::string& path) {
|
bool Load(const std::string& path) {
|
||||||
g_certs.clear();
|
Clear();
|
||||||
|
|
||||||
FileUtil::IOFile file(path, "rb");
|
FileUtil::IOFile file(path, "rb");
|
||||||
DataContainer container(file.GetData());
|
DataContainer container(file.GetData());
|
||||||
@@ -151,6 +151,11 @@ bool IsLoaded() {
|
|||||||
return g_is_loaded;
|
return g_is_loaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Clear() {
|
||||||
|
g_is_loaded = false;
|
||||||
|
g_certs.clear();
|
||||||
|
}
|
||||||
|
|
||||||
const Certificate& Get(const std::string& name) {
|
const Certificate& Get(const std::string& name) {
|
||||||
return g_certs.at(name);
|
return g_certs.at(name);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ namespace Certs {
|
|||||||
|
|
||||||
bool Load(const std::string& path);
|
bool Load(const std::string& path);
|
||||||
bool IsLoaded();
|
bool IsLoaded();
|
||||||
|
void Clear();
|
||||||
const Certificate& Get(const std::string& name);
|
const Certificate& Get(const std::string& name);
|
||||||
bool Exists(const std::string& name);
|
bool Exists(const std::string& name);
|
||||||
|
|
||||||
|
|||||||
@@ -447,19 +447,12 @@ bool NCCHContainer::DecryptToFile(std::shared_ptr<FileUtil::IOFile> dest_file,
|
|||||||
|
|
||||||
const auto size = file->GetSize();
|
const auto size = file->GetSize();
|
||||||
|
|
||||||
decryptor.Reset(size);
|
|
||||||
decryptor.SetCrypto(nullptr);
|
decryptor.SetCrypto(nullptr);
|
||||||
return decryptor.CryptAndWriteFile(file, size, dest_file, callback);
|
return decryptor.CryptAndWriteFile(file, size, dest_file, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto total_size = file->GetSize();
|
const auto total_size = file->GetSize();
|
||||||
decryptor.Reset(total_size); // This is inaccurate but doesn't really matter as we don't use it
|
|
||||||
|
|
||||||
std::size_t written{};
|
std::size_t written{};
|
||||||
const auto decryptor_callback = [total_size, &written, &callback](std::size_t current,
|
|
||||||
std::size_t /*total*/) {
|
|
||||||
callback(written + current, total_size);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Write NCCH header
|
// Write NCCH header
|
||||||
NCCH_Header modified_header = ncch_header;
|
NCCH_Header modified_header = ncch_header;
|
||||||
@@ -487,6 +480,7 @@ bool NCCHContainer::DecryptToFile(std::shared_ptr<FileUtil::IOFile> dest_file,
|
|||||||
written += sizeof(ExHeader_Header);
|
written += sizeof(ExHeader_Header);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Common::ProgressCallbackWrapper wrapper(total_size);
|
||||||
const auto Write = [&](std::string_view name, std::size_t offset, std::size_t size,
|
const auto Write = [&](std::string_view name, std::size_t offset, std::size_t size,
|
||||||
bool decrypt = false, const Key::AESKey& key = {},
|
bool decrypt = false, const Key::AESKey& key = {},
|
||||||
const Key::AESKey& ctr = {}, std::size_t aes_seek_pos = 0) {
|
const Key::AESKey& ctr = {}, std::size_t aes_seek_pos = 0) {
|
||||||
@@ -518,9 +512,10 @@ bool NCCHContainer::DecryptToFile(std::shared_ptr<FileUtil::IOFile> dest_file,
|
|||||||
}
|
}
|
||||||
|
|
||||||
written = offset;
|
written = offset;
|
||||||
|
wrapper.SetCurrent(written);
|
||||||
|
|
||||||
decryptor.SetCrypto(decrypt ? CreateCTRCrypto(key, ctr, aes_seek_pos) : nullptr);
|
decryptor.SetCrypto(decrypt ? CreateCTRCrypto(key, ctr, aes_seek_pos) : nullptr);
|
||||||
if (!decryptor.CryptAndWriteFile(file, size, dest_file, decryptor_callback)) {
|
if (!decryptor.CryptAndWriteFile(file, size, dest_file, wrapper.Wrap(callback))) {
|
||||||
LOG_ERROR(Core, "Could not write {}", name);
|
LOG_ERROR(Core, "Could not write {}", name);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ public:
|
|||||||
*/
|
*/
|
||||||
bool DecryptToFile(
|
bool DecryptToFile(
|
||||||
std::shared_ptr<FileUtil::IOFile> dest_file,
|
std::shared_ptr<FileUtil::IOFile> dest_file,
|
||||||
const Common::ProgressCallback& callback = [](std::size_t, std::size_t) {});
|
const Common::ProgressCallback& callback = [](u64, u64) {});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Aborts DecryptToFile. Simply aborts the decryptor.
|
* Aborts DecryptToFile. Simply aborts the decryptor.
|
||||||
|
|||||||
+36
-38
@@ -28,7 +28,11 @@ SDMCImporter::SDMCImporter(const Config& config_) : config(config_) {
|
|||||||
is_good = Init();
|
is_good = Init();
|
||||||
}
|
}
|
||||||
|
|
||||||
SDMCImporter::~SDMCImporter() = default;
|
SDMCImporter::~SDMCImporter() {
|
||||||
|
// Unload global DBs
|
||||||
|
Certs::Clear();
|
||||||
|
Seeds::Clear();
|
||||||
|
}
|
||||||
|
|
||||||
bool SDMCImporter::Init() {
|
bool SDMCImporter::Init() {
|
||||||
ASSERT_MSG(!config.sdmc_path.empty() && !config.user_path.empty() &&
|
ASSERT_MSG(!config.sdmc_path.empty() && !config.user_path.empty() &&
|
||||||
@@ -53,6 +57,7 @@ bool SDMCImporter::Init() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load global DBs
|
||||||
if (!config.seed_db_path.empty()) {
|
if (!config.seed_db_path.empty()) {
|
||||||
Seeds::Load(config.seed_db_path);
|
Seeds::Load(config.seed_db_path);
|
||||||
}
|
}
|
||||||
@@ -60,7 +65,9 @@ bool SDMCImporter::Init() {
|
|||||||
Certs::Load(config.certs_db_path);
|
Certs::Load(config.certs_db_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create children
|
||||||
sdmc_decryptor = std::make_unique<SDMCDecryptor>(config.sdmc_path);
|
sdmc_decryptor = std::make_unique<SDMCDecryptor>(config.sdmc_path);
|
||||||
|
cia_builder = std::make_unique<CIABuilder>(config);
|
||||||
|
|
||||||
// Load SDMC Title DB
|
// Load SDMC Title DB
|
||||||
{
|
{
|
||||||
@@ -138,15 +145,15 @@ bool SDMCImporter::ImportContentImpl(const ContentSpecifier& specifier,
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
template <typename Dec>
|
using DecryptionFunc = std::function<bool(const std::string&, const Common::ProgressCallback&)>;
|
||||||
bool ImportTitleGeneric(Dec& decryptor, const std::string& base_path,
|
bool ImportTitleGeneric(const std::string& base_path, const ContentSpecifier& specifier,
|
||||||
const ContentSpecifier& specifier,
|
const Common::ProgressCallback& callback,
|
||||||
const std::function<bool(const std::string&)>& decryption_func) {
|
const DecryptionFunc& decryption_func) {
|
||||||
|
|
||||||
decryptor.Reset(specifier.maximum_size);
|
Common::ProgressCallbackWrapper wrapper{specifier.maximum_size};
|
||||||
const FileUtil::DirectoryEntryCallable DirectoryEntryCallback =
|
const FileUtil::DirectoryEntryCallable DirectoryEntryCallback =
|
||||||
[size = base_path.size(), &DirectoryEntryCallback,
|
[size = base_path.size(), &DirectoryEntryCallback, &callback, &decryption_func,
|
||||||
&decryption_func](u64* /*num_entries_out*/, const std::string& directory,
|
&wrapper](u64* /*num_entries_out*/, const std::string& directory,
|
||||||
const std::string& virtual_name) {
|
const std::string& virtual_name) {
|
||||||
if (FileUtil::IsDirectory(directory + virtual_name + "/")) {
|
if (FileUtil::IsDirectory(directory + virtual_name + "/")) {
|
||||||
if (virtual_name == "cmd") {
|
if (virtual_name == "cmd") {
|
||||||
@@ -157,7 +164,7 @@ bool ImportTitleGeneric(Dec& decryptor, const std::string& base_path,
|
|||||||
DirectoryEntryCallback);
|
DirectoryEntryCallback);
|
||||||
}
|
}
|
||||||
const auto filepath = (directory + virtual_name).substr(size - 1);
|
const auto filepath = (directory + virtual_name).substr(size - 1);
|
||||||
return decryption_func(filepath);
|
return decryption_func(filepath, wrapper.Wrap(callback));
|
||||||
};
|
};
|
||||||
const auto path = fmt::format("title/{:08x}/{:08x}/content/", (specifier.id >> 32),
|
const auto path = fmt::format("title/{:08x}/{:08x}/content/", (specifier.id >> 32),
|
||||||
(specifier.id & 0xFFFFFFFF));
|
(specifier.id & 0xFFFFFFFF));
|
||||||
@@ -169,15 +176,15 @@ bool ImportTitleGeneric(Dec& decryptor, const std::string& base_path,
|
|||||||
bool SDMCImporter::ImportTitle(const ContentSpecifier& specifier,
|
bool SDMCImporter::ImportTitle(const ContentSpecifier& specifier,
|
||||||
const Common::ProgressCallback& callback) {
|
const Common::ProgressCallback& callback) {
|
||||||
return ImportTitleGeneric(
|
return ImportTitleGeneric(
|
||||||
*sdmc_decryptor, config.sdmc_path, specifier,
|
config.sdmc_path, specifier, callback,
|
||||||
[this, &callback](const std::string& filepath) {
|
[this](const std::string& filepath, const Common::ProgressCallback& wrapped_callback) {
|
||||||
return sdmc_decryptor->DecryptAndWriteFile(
|
return sdmc_decryptor->DecryptAndWriteFile(
|
||||||
filepath,
|
filepath,
|
||||||
FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) +
|
FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) +
|
||||||
"Nintendo "
|
"Nintendo "
|
||||||
"3DS/00000000000000000000000000000000/00000000000000000000000000000000" +
|
"3DS/00000000000000000000000000000000/00000000000000000000000000000000" +
|
||||||
filepath,
|
filepath,
|
||||||
callback);
|
wrapped_callback);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,8 +195,9 @@ bool SDMCImporter::ImportNandTitle(const ContentSpecifier& specifier,
|
|||||||
config.system_titles_path.substr(0, config.system_titles_path.size() - 6);
|
config.system_titles_path.substr(0, config.system_titles_path.size() - 6);
|
||||||
FileDecryptor decryptor;
|
FileDecryptor decryptor;
|
||||||
return ImportTitleGeneric(
|
return ImportTitleGeneric(
|
||||||
decryptor, base_path, specifier,
|
base_path, specifier, callback,
|
||||||
[&base_path, &decryptor, &callback](const std::string& filepath) {
|
[&base_path, &decryptor](const std::string& filepath,
|
||||||
|
const Common::ProgressCallback& wrapped_callback) {
|
||||||
const auto physical_path = base_path + filepath.substr(1);
|
const auto physical_path = base_path + filepath.substr(1);
|
||||||
const auto citra_path = FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) +
|
const auto citra_path = FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) +
|
||||||
"00000000000000000000000000000000" + filepath;
|
"00000000000000000000000000000000" + filepath;
|
||||||
@@ -201,7 +209,7 @@ bool SDMCImporter::ImportNandTitle(const ContentSpecifier& specifier,
|
|||||||
return decryptor.CryptAndWriteFile(
|
return decryptor.CryptAndWriteFile(
|
||||||
std::make_shared<FileUtil::IOFile>(physical_path, "rb"),
|
std::make_shared<FileUtil::IOFile>(physical_path, "rb"),
|
||||||
FileUtil::GetSize(physical_path),
|
FileUtil::GetSize(physical_path),
|
||||||
std::make_shared<FileUtil::IOFile>(citra_path, "wb"), callback);
|
std::make_shared<FileUtil::IOFile>(citra_path, "wb"), wrapped_callback);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -716,18 +724,9 @@ bool SDMCImporter::BuildCIA(CIABuildType build_type, const ContentSpecifier& spe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
bool ret = cia_builder->Init(build_type, destination, tmd, specifier.maximum_size, callback);
|
||||||
std::lock_guard lock{cia_builder_mutex};
|
|
||||||
cia_builder = std::make_unique<CIABuilder>();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ret = cia_builder->Init(build_type, destination, tmd, config,
|
|
||||||
FileUtil::GetDirectoryTreeSize(physical_path), callback);
|
|
||||||
SCOPE_EXIT({
|
SCOPE_EXIT({
|
||||||
{
|
cia_builder->Cleanup();
|
||||||
std::lock_guard lock{cia_builder_mutex};
|
|
||||||
cia_builder.reset(); // To release file handles, etc
|
|
||||||
}
|
|
||||||
if (!ret) { // Remove borked file
|
if (!ret) { // Remove borked file
|
||||||
FileUtil::Delete(destination);
|
FileUtil::Delete(destination);
|
||||||
}
|
}
|
||||||
@@ -782,11 +781,11 @@ bool SDMCImporter::BuildCIA(CIABuildType build_type, const ContentSpecifier& spe
|
|||||||
}
|
}
|
||||||
|
|
||||||
void SDMCImporter::AbortBuildCIA() {
|
void SDMCImporter::AbortBuildCIA() {
|
||||||
std::lock_guard lock{cia_builder_mutex};
|
|
||||||
if (cia_builder) {
|
|
||||||
cia_builder->Abort();
|
cia_builder->Abort();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Add a certain amount to the titles' maximum sizes, so that they are always larger than CIA sizes
|
||||||
|
constexpr u64 TitleSizeAllowance = 0xA000;
|
||||||
|
|
||||||
void SDMCImporter::ListTitle(std::vector<ContentSpecifier>& out) const {
|
void SDMCImporter::ListTitle(std::vector<ContentSpecifier>& out) const {
|
||||||
const auto ProcessDirectory = [this, &out, &sdmc_path = config.sdmc_path](ContentType type,
|
const auto ProcessDirectory = [this, &out, &sdmc_path = config.sdmc_path](ContentType type,
|
||||||
@@ -836,9 +835,10 @@ void SDMCImporter::ListTitle(std::vector<ContentSpecifier>& out) const {
|
|||||||
|
|
||||||
const auto& [name, extdata_id, encryption, seed_crypto, icon] =
|
const auto& [name, extdata_id, encryption, seed_crypto, icon] =
|
||||||
LoadTitleData(ncch);
|
LoadTitleData(ncch);
|
||||||
out.push_back(
|
const auto size =
|
||||||
{type, id, FileUtil::Exists(citra_path + "content/"),
|
FileUtil::GetDirectoryTreeSize(directory + virtual_name + "/content/") +
|
||||||
FileUtil::GetDirectoryTreeSize(directory + virtual_name + "/content/"),
|
TitleSizeAllowance;
|
||||||
|
out.push_back({type, id, FileUtil::Exists(citra_path + "content/"), size,
|
||||||
name, extdata_id, encryption, seed_crypto, icon});
|
name, extdata_id, encryption, seed_crypto, icon});
|
||||||
} while (false);
|
} while (false);
|
||||||
}
|
}
|
||||||
@@ -907,9 +907,6 @@ void SDMCImporter::ListNandTitle(std::vector<ContentSpecifier>& out) const {
|
|||||||
std::make_shared<FileUtil::IOFile>(boot_content_path, "rb"));
|
std::make_shared<FileUtil::IOFile>(boot_content_path, "rb"));
|
||||||
if (!ncch.Load()) {
|
if (!ncch.Load()) {
|
||||||
LOG_WARNING(Core, "Could not load NCCH {}", boot_content_path);
|
LOG_WARNING(Core, "Could not load NCCH {}", boot_content_path);
|
||||||
out.push_back({ContentType::SystemTitle, id,
|
|
||||||
FileUtil::Exists(citra_path + "content/"),
|
|
||||||
FileUtil::GetDirectoryTreeSize(content_path)});
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -917,9 +914,10 @@ void SDMCImporter::ListNandTitle(std::vector<ContentSpecifier>& out) const {
|
|||||||
LoadTitleData(ncch);
|
LoadTitleData(ncch);
|
||||||
const auto type = (id >> 32) == 0x00040030 ? ContentType::SystemApplet
|
const auto type = (id >> 32) == 0x00040030 ? ContentType::SystemApplet
|
||||||
: ContentType::SystemTitle;
|
: ContentType::SystemTitle;
|
||||||
out.push_back(
|
const auto size =
|
||||||
{type, id, FileUtil::Exists(citra_path + "content/"),
|
FileUtil::GetDirectoryTreeSize(directory + virtual_name + "/content/") +
|
||||||
FileUtil::GetDirectoryTreeSize(directory + virtual_name + "/content/"),
|
TitleSizeAllowance;
|
||||||
|
out.push_back({type, id, FileUtil::Exists(citra_path + "content/"), size,
|
||||||
name, extdata_id, encryption, seed_crypto, icon});
|
name, extdata_id, encryption, seed_crypto, icon});
|
||||||
} while (false);
|
} while (false);
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -6,7 +6,6 @@
|
|||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
@@ -17,6 +16,7 @@ namespace Core {
|
|||||||
|
|
||||||
class CIABuilder;
|
class CIABuilder;
|
||||||
class SDMCDecryptor;
|
class SDMCDecryptor;
|
||||||
|
class TicketDB;
|
||||||
class TitleDB;
|
class TitleDB;
|
||||||
class TitleMetadata;
|
class TitleMetadata;
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ public:
|
|||||||
*/
|
*/
|
||||||
bool ImportContent(
|
bool ImportContent(
|
||||||
const ContentSpecifier& specifier,
|
const ContentSpecifier& specifier,
|
||||||
const Common::ProgressCallback& callback = [](std::size_t, std::size_t) {});
|
const Common::ProgressCallback& callback = [](u64, u64) {});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Aborts current importing.
|
* Aborts current importing.
|
||||||
@@ -175,7 +175,7 @@ private:
|
|||||||
// Impl of ImportContent without deleting mechanism.
|
// Impl of ImportContent without deleting mechanism.
|
||||||
bool ImportContentImpl(
|
bool ImportContentImpl(
|
||||||
const ContentSpecifier& specifier,
|
const ContentSpecifier& specifier,
|
||||||
const Common::ProgressCallback& callback = [](std::size_t, std::size_t) {});
|
const Common::ProgressCallback& callback = [](u64, u64) {});
|
||||||
bool ImportTitle(const ContentSpecifier& specifier, const Common::ProgressCallback& callback);
|
bool ImportTitle(const ContentSpecifier& specifier, const Common::ProgressCallback& callback);
|
||||||
bool ImportNandTitle(const ContentSpecifier& specifier,
|
bool ImportNandTitle(const ContentSpecifier& specifier,
|
||||||
const Common::ProgressCallback& callback);
|
const Common::ProgressCallback& callback);
|
||||||
@@ -207,8 +207,8 @@ private:
|
|||||||
Config config;
|
Config config;
|
||||||
std::unique_ptr<SDMCDecryptor> sdmc_decryptor;
|
std::unique_ptr<SDMCDecryptor> sdmc_decryptor;
|
||||||
|
|
||||||
|
// Used for CIA building
|
||||||
std::unique_ptr<CIABuilder> cia_builder;
|
std::unique_ptr<CIABuilder> cia_builder;
|
||||||
std::mutex cia_builder_mutex;
|
|
||||||
|
|
||||||
// The NCCH used to dump CXIs.
|
// The NCCH used to dump CXIs.
|
||||||
std::unique_ptr<NCCHContainer> dump_cxi_ncch;
|
std::unique_ptr<NCCHContainer> dump_cxi_ncch;
|
||||||
|
|||||||
@@ -48,10 +48,6 @@ std::array<u8, 16> GetFileCTR(const std::string& path) {
|
|||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void SDMCDecryptor::Reset(std::size_t total_size) {
|
|
||||||
file_decryptor.Reset(total_size);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SDMCDecryptor::DecryptAndWriteFile(const std::string& source, const std::string& destination,
|
bool SDMCDecryptor::DecryptAndWriteFile(const std::string& source, const std::string& destination,
|
||||||
const Common::ProgressCallback& callback) {
|
const Common::ProgressCallback& callback) {
|
||||||
if (!FileUtil::CreateFullPath(destination)) {
|
if (!FileUtil::CreateFullPath(destination)) {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ public:
|
|||||||
*/
|
*/
|
||||||
bool DecryptAndWriteFile(
|
bool DecryptAndWriteFile(
|
||||||
const std::string& source, const std::string& destination,
|
const std::string& source, const std::string& destination,
|
||||||
const Common::ProgressCallback& callback = [](std::size_t, std::size_t) {});
|
const Common::ProgressCallback& callback = [](u64, u64) {});
|
||||||
|
|
||||||
void Abort();
|
void Abort();
|
||||||
|
|
||||||
@@ -43,14 +43,6 @@ public:
|
|||||||
*/
|
*/
|
||||||
std::vector<u8> DecryptFile(const std::string& source) const;
|
std::vector<u8> DecryptFile(const std::string& source) const;
|
||||||
|
|
||||||
/**
|
|
||||||
* Marks the beginning of a new content, resetting imported_size counter, and setting an new
|
|
||||||
* total_size for the next content.
|
|
||||||
* This doesn't affect at all how the contents will be imported, but will make sure the callback
|
|
||||||
* is properly invoked.
|
|
||||||
*/
|
|
||||||
void Reset(std::size_t total_size);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string root_folder;
|
std::string root_folder;
|
||||||
FileDecryptor file_decryptor;
|
FileDecryptor file_decryptor;
|
||||||
|
|||||||
@@ -19,35 +19,35 @@ void MultiJob::run() {
|
|||||||
total_size += content.maximum_size;
|
total_size += content.maximum_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
u64 size_imported = 0, count = 0;
|
std::size_t count = 0;
|
||||||
int eta = -1;
|
int eta = -1;
|
||||||
|
|
||||||
const auto initial_time = std::chrono::steady_clock::now();
|
const auto initial_time = std::chrono::steady_clock::now();
|
||||||
const auto UpdateETA = [total_size, &eta, initial_time](u64 size_imported) {
|
const auto UpdateETA = [total_size, &eta, initial_time](u64 size_imported) {
|
||||||
if (size_imported >= 10 * 1024 * 1024) { // 10M Threshold
|
if (size_imported < 10 * 1024 * 1024) { // 10M Threshold
|
||||||
|
return;
|
||||||
|
}
|
||||||
using namespace std::chrono;
|
using namespace std::chrono;
|
||||||
const u64 time_elapsed =
|
const u64 time_elapsed =
|
||||||
duration_cast<milliseconds>(steady_clock::now() - initial_time).count();
|
duration_cast<milliseconds>(steady_clock::now() - initial_time).count();
|
||||||
eta = static_cast<int>(time_elapsed * (total_size - size_imported) / (size_imported) /
|
eta =
|
||||||
1000);
|
static_cast<int>(time_elapsed * (total_size - size_imported) / (size_imported) / 1000);
|
||||||
}
|
};
|
||||||
|
const auto Callback = [this, &eta, &UpdateETA](u64 current_imported_size,
|
||||||
|
u64 total_imported_size, u64 /*total_size*/) {
|
||||||
|
UpdateETA(total_imported_size);
|
||||||
|
emit ProgressUpdated(current_imported_size, total_imported_size, eta);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Common::ProgressCallbackWrapper wrapper(total_size);
|
||||||
for (const auto& content : contents) {
|
for (const auto& content : contents) {
|
||||||
emit NextContent(size_imported, count + 1, content, eta);
|
emit NextContent(count + 1, content, eta);
|
||||||
const auto callback = [this, size_imported, &eta, &UpdateETA](std::size_t current_size,
|
if (!execute_func(importer, content, wrapper.Wrap(Callback))) {
|
||||||
std::size_t /*total_size*/) {
|
|
||||||
UpdateETA(size_imported + current_size);
|
|
||||||
emit ProgressUpdated(size_imported + current_size, current_size, eta);
|
|
||||||
};
|
|
||||||
if (!execute_func(importer, content, callback)) {
|
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
failed_contents.emplace_back(content);
|
failed_contents.emplace_back(content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
count++;
|
count++;
|
||||||
size_imported += content.maximum_size;
|
|
||||||
UpdateETA(size_imported);
|
|
||||||
|
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -31,16 +31,15 @@ public:
|
|||||||
signals:
|
signals:
|
||||||
/**
|
/**
|
||||||
* Called when progress is updated on the current content.
|
* Called when progress is updated on the current content.
|
||||||
* @param total_size_imported Total imported size taking all previous contents into
|
* @param current_imported_size Imported size of the current content.
|
||||||
|
* @param total_imported_size Total imported size taking all previous contents into
|
||||||
* consideration.
|
* consideration.
|
||||||
* @param current_size_imported Imported size of the current content.
|
|
||||||
* @param eta ETA in seconds, 0 when not determined.
|
* @param eta ETA in seconds, 0 when not determined.
|
||||||
*/
|
*/
|
||||||
void ProgressUpdated(u64 total_size_imported, u64 current_size_imported, int eta);
|
void ProgressUpdated(u64 current_imported_size, u64 total_imported_size, int eta);
|
||||||
|
|
||||||
/// Dumping of a content has been finished, go on to the next. Called at start as well.
|
/// Dumping of a content has been finished, go on to the next. Called at start as well.
|
||||||
void NextContent(u64 size_imported, u64 count, const Core::ContentSpecifier& next_content,
|
void NextContent(std::size_t count, const Core::ContentSpecifier& next_content, int eta);
|
||||||
int eta);
|
|
||||||
|
|
||||||
void Completed();
|
void Completed();
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ SimpleJob::SimpleJob(QObject* parent, ExecuteFunc execute_, AbortFunc abort_)
|
|||||||
SimpleJob::~SimpleJob() = default;
|
SimpleJob::~SimpleJob() = default;
|
||||||
|
|
||||||
void SimpleJob::run() {
|
void SimpleJob::run() {
|
||||||
const bool ret = execute(
|
const bool ret =
|
||||||
[this](std::size_t current, std::size_t total) { emit ProgressUpdated(current, total); });
|
execute([this](u64 current, u64 total) { emit ProgressUpdated(current, total); });
|
||||||
|
|
||||||
if (ret || canceled) {
|
if (ret || canceled) {
|
||||||
emit Completed();
|
emit Completed();
|
||||||
|
|||||||
@@ -93,11 +93,11 @@ static QPixmap GetContentIcon(const Core::ContentSpecifier& specifier,
|
|||||||
QImage::Format::Format_RGB16));
|
QImage::Format::Format_RGB16));
|
||||||
}
|
}
|
||||||
|
|
||||||
ImportDialog::ImportDialog(QWidget* parent, const Core::Config& config)
|
ImportDialog::ImportDialog(QWidget* parent, const Core::Config& config_)
|
||||||
: QDialog(parent), ui(std::make_unique<Ui::ImportDialog>()), user_path(config.user_path),
|
: QDialog(parent), ui(std::make_unique<Ui::ImportDialog>()), config(config_) {
|
||||||
has_cert_db(!config.certs_db_path.empty()), importer(config) {
|
|
||||||
|
|
||||||
qRegisterMetaType<u64>("u64");
|
qRegisterMetaType<u64>("u64");
|
||||||
|
qRegisterMetaType<std::size_t>("std::size_t");
|
||||||
qRegisterMetaType<Core::ContentSpecifier>();
|
qRegisterMetaType<Core::ContentSpecifier>();
|
||||||
|
|
||||||
ui->setupUi(this);
|
ui->setupUi(this);
|
||||||
@@ -105,12 +105,8 @@ ImportDialog::ImportDialog(QWidget* parent, const Core::Config& config)
|
|||||||
const double scale = qApp->desktop()->logicalDpiX() / 96.0;
|
const double scale = qApp->desktop()->logicalDpiX() / 96.0;
|
||||||
resize(static_cast<int>(width() * scale), static_cast<int>(height() * scale));
|
resize(static_cast<int>(width() * scale), static_cast<int>(height() * scale));
|
||||||
|
|
||||||
if (!importer.IsGood()) {
|
RelistContent();
|
||||||
QMessageBox::critical(
|
UpdateSizeDisplay();
|
||||||
this, tr("Importer Error"),
|
|
||||||
tr("Failed to initalize the importer.\nRefer to the log for details."));
|
|
||||||
reject();
|
|
||||||
}
|
|
||||||
|
|
||||||
ui->title_view_button->setChecked(true);
|
ui->title_view_button->setChecked(true);
|
||||||
|
|
||||||
@@ -128,9 +124,6 @@ ImportDialog::ImportDialog(QWidget* parent, const Core::Config& config)
|
|||||||
connect(ui->title_view_button, &QRadioButton::toggled, this, &ImportDialog::RepopulateContent);
|
connect(ui->title_view_button, &QRadioButton::toggled, this, &ImportDialog::RepopulateContent);
|
||||||
connect(ui->advanced_button, &QPushButton::clicked, this, &ImportDialog::ShowAdvancedMenu);
|
connect(ui->advanced_button, &QPushButton::clicked, this, &ImportDialog::ShowAdvancedMenu);
|
||||||
|
|
||||||
RelistContent();
|
|
||||||
UpdateSizeDisplay();
|
|
||||||
|
|
||||||
// Set up column widths.
|
// Set up column widths.
|
||||||
// These values are tweaked with regard to the default dialog size.
|
// These values are tweaked with regard to the default dialog size.
|
||||||
ui->main->setColumnWidth(0, width() * 0.11);
|
ui->main->setColumnWidth(0, width() * 0.11);
|
||||||
@@ -155,11 +148,24 @@ void ImportDialog::RelistContent() {
|
|||||||
auto* future_watcher = new FutureWatcher(this);
|
auto* future_watcher = new FutureWatcher(this);
|
||||||
connect(future_watcher, &FutureWatcher::finished, this, [this, dialog] {
|
connect(future_watcher, &FutureWatcher::finished, this, [this, dialog] {
|
||||||
dialog->hide();
|
dialog->hide();
|
||||||
|
if (importer->IsGood()) {
|
||||||
RepopulateContent();
|
RepopulateContent();
|
||||||
|
} else {
|
||||||
|
QMessageBox::critical(
|
||||||
|
this, tr("Importer Error"),
|
||||||
|
tr("Failed to initalize the importer.\nRefer to the log for details."));
|
||||||
|
reject();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
auto future = QtConcurrent::run([&contents = this->contents, &importer = this->importer] {
|
auto future = QtConcurrent::run(
|
||||||
contents = importer.ListContent();
|
[&importer = this->importer, &config = this->config, &contents = this->contents] {
|
||||||
|
if (!importer) {
|
||||||
|
importer = std::make_unique<Core::SDMCImporter>(config);
|
||||||
|
}
|
||||||
|
if (importer->IsGood()) {
|
||||||
|
contents = importer->ListContent();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
future_watcher->setFuture(future);
|
future_watcher->setFuture(future);
|
||||||
}
|
}
|
||||||
@@ -323,6 +329,12 @@ void ImportDialog::InsertSecondLevelItem(std::size_t row, const Core::ContentSpe
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ImportDialog::RepopulateContent() {
|
void ImportDialog::RepopulateContent() {
|
||||||
|
if (contents.empty()) { // why???
|
||||||
|
QMessageBox::warning(this, tr("threeSD"), tr("Sorry, there are no contents available."));
|
||||||
|
reject();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
total_selected_size = 0;
|
total_selected_size = 0;
|
||||||
ui->main->clear();
|
ui->main->clear();
|
||||||
ui->main->setSortingEnabled(false);
|
ui->main->setSortingEnabled(false);
|
||||||
@@ -418,13 +430,14 @@ void ImportDialog::RepopulateContent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ImportDialog::UpdateSizeDisplay() {
|
void ImportDialog::UpdateSizeDisplay() {
|
||||||
QStorageInfo storage(QString::fromStdString(user_path));
|
QStorageInfo storage(QString::fromStdString(config.user_path));
|
||||||
if (!storage.isValid() || !storage.isReady()) {
|
if (!storage.isValid() || !storage.isReady()) {
|
||||||
LOG_ERROR(Frontend, "Storage {} is not good", user_path);
|
LOG_ERROR(Frontend, "Storage {} is not good", config.user_path);
|
||||||
QMessageBox::critical(
|
QMessageBox::critical(
|
||||||
this, tr("Bad Storage"),
|
this, tr("Bad Storage"),
|
||||||
tr("An error occured while trying to get available space for the storage."));
|
tr("An error occured while trying to get available space for the storage."));
|
||||||
reject();
|
reject();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ui->availableSpace->setText(
|
ui->availableSpace->setText(
|
||||||
@@ -602,9 +615,8 @@ void ImportDialog::RunMultiJob(MultiJob* job, std::size_t total_count, u64 total
|
|||||||
dialog->setMinimumDuration(0);
|
dialog->setMinimumDuration(0);
|
||||||
|
|
||||||
connect(job, &MultiJob::NextContent, this,
|
connect(job, &MultiJob::NextContent, this,
|
||||||
[this, bar, dialog, multiplier, total_count](
|
[this, bar, dialog, multiplier,
|
||||||
u64 size_imported, u64 count, const Core::ContentSpecifier& next_content, int eta) {
|
total_count](std::size_t count, const Core::ContentSpecifier& next_content, int eta) {
|
||||||
bar->setValue(static_cast<int>(size_imported / multiplier));
|
|
||||||
dialog->setLabelText(
|
dialog->setLabelText(
|
||||||
tr("<p>(%1/%2) %3 (%4)</p><p> </p><p align=\"right\">%5</p>")
|
tr("<p>(%1/%2) %3 (%4)</p><p> </p><p align=\"right\">%5</p>")
|
||||||
.arg(count)
|
.arg(count)
|
||||||
@@ -616,16 +628,16 @@ void ImportDialog::RunMultiJob(MultiJob* job, std::size_t total_count, u64 total
|
|||||||
current_count = count;
|
current_count = count;
|
||||||
});
|
});
|
||||||
connect(job, &MultiJob::ProgressUpdated, this,
|
connect(job, &MultiJob::ProgressUpdated, this,
|
||||||
[this, bar, dialog, multiplier, total_count](u64 total_size_imported,
|
[this, bar, dialog, multiplier, total_count](u64 current_imported_size,
|
||||||
u64 current_size_imported, int eta) {
|
u64 total_imported_size, int eta) {
|
||||||
bar->setValue(static_cast<int>(total_size_imported / multiplier));
|
bar->setValue(static_cast<int>(total_imported_size / multiplier));
|
||||||
dialog->setLabelText(tr("<p>(%1/%2) %3 (%4)</p><p align=\"center\">%5 "
|
dialog->setLabelText(tr("<p>(%1/%2) %3 (%4)</p><p align=\"center\">%5 "
|
||||||
"/ %6</p><p align=\"right\">%7</p>")
|
"/ %6</p><p align=\"right\">%7</p>")
|
||||||
.arg(current_count)
|
.arg(current_count)
|
||||||
.arg(total_count)
|
.arg(total_count)
|
||||||
.arg(GetContentName(current_content))
|
.arg(GetContentName(current_content))
|
||||||
.arg(GetContentTypeName(current_content.type))
|
.arg(GetContentTypeName(current_content.type))
|
||||||
.arg(ReadableByteSize(current_size_imported))
|
.arg(ReadableByteSize(current_imported_size))
|
||||||
.arg(ReadableByteSize(current_content.maximum_size))
|
.arg(ReadableByteSize(current_content.maximum_size))
|
||||||
.arg(FormatETA(eta)));
|
.arg(FormatETA(eta)));
|
||||||
});
|
});
|
||||||
@@ -709,7 +721,7 @@ void ImportDialog::StartImporting() {
|
|||||||
const std::size_t total_count = to_import.size();
|
const std::size_t total_count = to_import.size();
|
||||||
|
|
||||||
auto* job =
|
auto* job =
|
||||||
new MultiJob(this, importer, std::move(to_import), &Core::SDMCImporter::ImportContent,
|
new MultiJob(this, *importer, std::move(to_import), &Core::SDMCImporter::ImportContent,
|
||||||
&Core::SDMCImporter::AbortImporting);
|
&Core::SDMCImporter::AbortImporting);
|
||||||
|
|
||||||
RunMultiJob(job, total_count, total_selected_size);
|
RunMultiJob(job, total_count, total_selected_size);
|
||||||
@@ -728,9 +740,9 @@ void ImportDialog::StartDumpingCXISingle(const Core::ContentSpecifier& specifier
|
|||||||
auto* job = new SimpleJob(
|
auto* job = new SimpleJob(
|
||||||
this,
|
this,
|
||||||
[this, specifier, path](const Common::ProgressCallback& callback) {
|
[this, specifier, path](const Common::ProgressCallback& callback) {
|
||||||
return importer.DumpCXI(specifier, path.toStdString(), callback);
|
return importer->DumpCXI(specifier, path.toStdString(), callback);
|
||||||
},
|
},
|
||||||
[this] { importer.AbortDumpCXI(); });
|
[this] { importer->AbortDumpCXI(); });
|
||||||
RunSimpleJob(job);
|
RunSimpleJob(job);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -771,13 +783,12 @@ void ImportDialog::StartBatchDumpingCXI() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const auto total_count = to_import.size();
|
const auto total_count = to_import.size();
|
||||||
const auto total_size =
|
const auto total_size = std::accumulate(to_import.begin(), to_import.end(), u64{0},
|
||||||
std::accumulate(to_import.begin(), to_import.end(), std::size_t{0},
|
[](u64 sum, const Core::ContentSpecifier& specifier) {
|
||||||
[](std::size_t sum, const Core::ContentSpecifier& specifier) {
|
|
||||||
return sum + specifier.maximum_size;
|
return sum + specifier.maximum_size;
|
||||||
});
|
});
|
||||||
auto* job = new MultiJob(
|
auto* job = new MultiJob(
|
||||||
this, importer, std::move(to_import),
|
this, *importer, std::move(to_import),
|
||||||
[path](Core::SDMCImporter& importer, const Core::ContentSpecifier& specifier,
|
[path](Core::SDMCImporter& importer, const Core::ContentSpecifier& specifier,
|
||||||
const Common::ProgressCallback& callback) {
|
const Common::ProgressCallback& callback) {
|
||||||
return importer.DumpCXI(specifier, path.toStdString(), callback, true);
|
return importer.DumpCXI(specifier, path.toStdString(), callback, true);
|
||||||
@@ -792,7 +803,7 @@ void ImportDialog::StartBuildingCIASingle(const Core::ContentSpecifier& specifie
|
|||||||
CIABuildDialog dialog(this,
|
CIABuildDialog dialog(this,
|
||||||
/*is_dir*/ false,
|
/*is_dir*/ false,
|
||||||
/*is_nand*/ specifier.type == Core::ContentType::SystemTitle,
|
/*is_nand*/ specifier.type == Core::ContentType::SystemTitle,
|
||||||
/*enable_legit*/ importer.CanBuildLegitCIA(specifier),
|
/*enable_legit*/ importer->CanBuildLegitCIA(specifier),
|
||||||
last_build_cia_path);
|
last_build_cia_path);
|
||||||
if (dialog.exec() != QDialog::Accepted) {
|
if (dialog.exec() != QDialog::Accepted) {
|
||||||
return;
|
return;
|
||||||
@@ -803,9 +814,9 @@ void ImportDialog::StartBuildingCIASingle(const Core::ContentSpecifier& specifie
|
|||||||
auto* job = new SimpleJob(
|
auto* job = new SimpleJob(
|
||||||
this,
|
this,
|
||||||
[this, specifier, path = path, type = type](const Common::ProgressCallback& callback) {
|
[this, specifier, path = path, type = type](const Common::ProgressCallback& callback) {
|
||||||
return importer.BuildCIA(type, specifier, path.toStdString(), callback);
|
return importer->BuildCIA(type, specifier, path.toStdString(), callback);
|
||||||
},
|
},
|
||||||
[this] { importer.AbortBuildCIA(); });
|
[this] { importer->AbortBuildCIA(); });
|
||||||
RunSimpleJob(job);
|
RunSimpleJob(job);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -845,7 +856,7 @@ void ImportDialog::StartBatchBuildingCIA() {
|
|||||||
});
|
});
|
||||||
const bool enable_legit = std::all_of(to_import.begin(), to_import.end(),
|
const bool enable_legit = std::all_of(to_import.begin(), to_import.end(),
|
||||||
[this](const Core::ContentSpecifier& specifier) {
|
[this](const Core::ContentSpecifier& specifier) {
|
||||||
return importer.CanBuildLegitCIA(specifier);
|
return importer->CanBuildLegitCIA(specifier);
|
||||||
});
|
});
|
||||||
CIABuildDialog dialog(this, /*is_dir*/ true, is_nand, enable_legit, last_batch_build_cia_path);
|
CIABuildDialog dialog(this, /*is_dir*/ true, is_nand, enable_legit, last_batch_build_cia_path);
|
||||||
if (dialog.exec() != QDialog::Accepted) {
|
if (dialog.exec() != QDialog::Accepted) {
|
||||||
@@ -858,13 +869,12 @@ void ImportDialog::StartBatchBuildingCIA() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const auto total_count = to_import.size();
|
const auto total_count = to_import.size();
|
||||||
const auto total_size =
|
const auto total_size = std::accumulate(to_import.begin(), to_import.end(), u64{0},
|
||||||
std::accumulate(to_import.begin(), to_import.end(), std::size_t{0},
|
[](u64 sum, const Core::ContentSpecifier& specifier) {
|
||||||
[](std::size_t sum, const Core::ContentSpecifier& specifier) {
|
|
||||||
return sum + specifier.maximum_size;
|
return sum + specifier.maximum_size;
|
||||||
});
|
});
|
||||||
auto* job = new MultiJob(
|
auto* job = new MultiJob(
|
||||||
this, importer, std::move(to_import),
|
this, *importer, std::move(to_import),
|
||||||
[path = path, type = type](Core::SDMCImporter& importer,
|
[path = path, type = type](Core::SDMCImporter& importer,
|
||||||
const Core::ContentSpecifier& specifier,
|
const Core::ContentSpecifier& specifier,
|
||||||
const Common::ProgressCallback& callback) {
|
const Common::ProgressCallback& callback) {
|
||||||
|
|||||||
@@ -63,9 +63,9 @@ private:
|
|||||||
|
|
||||||
std::unique_ptr<Ui::ImportDialog> ui;
|
std::unique_ptr<Ui::ImportDialog> ui;
|
||||||
|
|
||||||
std::string user_path;
|
std::unique_ptr<Core::SDMCImporter> importer;
|
||||||
bool has_cert_db = false;
|
const Core::Config config;
|
||||||
Core::SDMCImporter importer;
|
|
||||||
std::vector<Core::ContentSpecifier> contents;
|
std::vector<Core::ContentSpecifier> contents;
|
||||||
u64 total_selected_size = 0;
|
u64 total_selected_size = 0;
|
||||||
|
|
||||||
@@ -84,5 +84,5 @@ private:
|
|||||||
|
|
||||||
// TODO: Why this won't work as locals?
|
// TODO: Why this won't work as locals?
|
||||||
Core::ContentSpecifier current_content = {};
|
Core::ContentSpecifier current_content = {};
|
||||||
u64 current_count = 0;
|
std::size_t current_count = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user