diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/src/core/cia_builder.cpp b/src/core/cia_builder.cpp index 3640535..a896c04 100644 --- a/src/core/cia_builder.cpp +++ b/src/core/cia_builder.cpp @@ -1,395 +1,395 @@ -// Copyright 2020 Pengfei Zhu -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include -#include -#include -#include "common/alignment.h" -#include "core/cia_builder.h" -#include "core/db/title_db.h" -#include "core/db/title_keys_bin.h" -#include "core/file_sys/certificate.h" -#include "core/file_sys/cia_common.h" -#include "core/file_sys/ticket.h" -#include "core/file_sys/title_metadata.h" -#include "core/importer.h" - -namespace Core { - -constexpr std::size_t CIA_ALIGNMENT = 0x40; - -class HashedFile : public FileUtil::IOFile { -public: - explicit HashedFile(const std::string& filename, const char openmode[], int flags = 0) - : FileUtil::IOFile(filename, openmode, flags) {} - ~HashedFile() override = default; - - void SetHashEnabled(bool enabled) { - hash_enabled = enabled; - if (enabled) { // Restart when hash is newly restarted - sha.Restart(); - } - } - - void GetHash(u8* out) { - sha.Final(out); - } - - bool VerifyHash(u8* out) { - return sha.Verify(out); - } - - std::size_t Write(const char* data, std::size_t length) override { - const std::size_t length_written = FileUtil::IOFile::Write(data, length); - if (hash_enabled) { - sha.Update(reinterpret_cast(data), length_written); - } - return length_written; - } - -private: - CryptoPP::SHA256 sha; - bool hash_enabled{}; -}; - -CIABuilder::CIABuilder(const Config& config, std::shared_ptr ticket_db_) - : ticket_db(std::move(ticket_db_)) { - if (!config.enc_title_keys_bin_path.empty()) { - enc_title_keys_bin = std::make_unique(); - 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; - -bool CIABuilder::Init(CIABuildType type_, const std::string& destination, TitleMetadata tmd_, - std::size_t total_size_, const Common::ProgressCallback& callback_) { - - type = type_; - header = {}; - meta = {}; - - if (!FileUtil::CreateFullPath(destination)) { - LOG_ERROR(Core, "Could not create {}", destination); - return false; - } - file = std::make_shared(destination, "wb"); - if (!*file) { - LOG_ERROR(Core, "Could not open file {}", destination); - return false; - } - - tmd = std::move(tmd_); - if (type == CIABuildType::Standard) { - // Remove encrypted flag from TMD chunks - for (auto& chunk : tmd.tmd_chunks) { - chunk.type &= ~0x01; - } - } - if (type == CIABuildType::Legit || type == CIABuildType::PirateLegit) { - // Check for legit TMD - if (!tmd.VerifyHashes() || !tmd.ValidateSignature()) { - LOG_ERROR(Core, "TMD is not legit"); - return false; - } - } - - header.header_size = sizeof(header); - // Header will be written in Finalize - - // Cert - cert_offset = Common::AlignUp(header.header_size, CIA_ALIGNMENT); - header.cert_size = CIA_CERT_SIZE; - if (!WriteCert()) { - LOG_ERROR(Core, "Could not write cert to file {}", destination); - return false; - } - - // Ticket - ticket_offset = Common::AlignUp(cert_offset + header.cert_size, CIA_ALIGNMENT); - if (!WriteTicket()) { - return false; - } - - // TMD will be written in Finalize (we need to set content hash, etc) - tmd_offset = Common::AlignUp(ticket_offset + header.tik_size, CIA_ALIGNMENT); - header.tmd_size = tmd.GetSize(); - - content_offset = Common::AlignUp(tmd_offset + header.tmd_size, CIA_ALIGNMENT); - header.content_size = 0; - - // Meta will be written in Finalize - header.meta_size = 0; - - // Initialize variables - written = content_offset; - total_size = total_size_; - - callback = callback_; - wrapper.total_size = total_size; - wrapper.SetCurrent(written); - - callback(written, total_size); - return true; -} - -void CIABuilder::Cleanup() { - file.reset(); -} - -bool CIABuilder::WriteCert() { - if (!Certs::IsLoaded()) { - return false; - } - - file->Seek(cert_offset, SEEK_SET); - for (const auto& cert : CIACertNames) { - if (!Certs::Get(cert).Save(*file)) { - LOG_ERROR(Core, "Failed to write cert {}", cert); - return false; - } - } - return true; -} - -bool CIABuilder::FindLegitTicket(Ticket& ticket, u64 title_id) const { - if (ticket_db && ticket_db->tickets.count(title_id)) { - ticket = ticket_db->tickets.at(title_id); - if (!ticket.ValidateSignature()) { - LOG_ERROR(Core, "Ticket in ticket.db for {:016x} is not legit", title_id); - return false; - } - return true; - } - - LOG_ERROR(Core, "Ticket for {:016x} does not exist in ticket.db", title_id); - return false; -} - -Ticket CIABuilder::BuildStandardTicket(u64 title_id) const { - Ticket ticket = BuildFakeTicket(title_id); - - // 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 - if (ticket_db && ticket_db->tickets.count(title_id)) { // ticket.db - const auto& legit_ticket = ticket_db->tickets.at(title_id); - ticket.body.common_key_index = legit_ticket.body.common_key_index; - ticket.body.title_key = legit_ticket.body.title_key; - } else if (enc_title_keys_bin && enc_title_keys_bin->count(title_id)) { // support files - const auto& entry = enc_title_keys_bin->at(title_id); - ticket.body.common_key_index = entry.common_key_index; - ticket.body.title_key = entry.title_key; - } else { - LOG_WARNING(Core, "Could not find title key for {:016x}", title_id); - } - return ticket; -} - -static Key::AESKey GetTitleKey(const Ticket& ticket) { - Key::SelectCommonKeyIndex(ticket.body.common_key_index); - if (!Key::IsNormalKeyAvailable(Key::TicketCommonKey)) { - LOG_ERROR(Core, "Ticket common key is not available"); - return {}; - } - - const auto ticket_key = Key::GetNormalKey(Key::TicketCommonKey); - Key::AESKey ctr{}; - std::memcpy(ctr.data(), &ticket.body.title_id, 8); - - CryptoPP::CBC_Mode::Decryption aes; - aes.SetKeyWithIV(ticket_key.data(), ticket_key.size(), ctr.data()); - - Key::AESKey title_key = ticket.body.title_key; - aes.ProcessData(title_key.data(), title_key.data(), title_key.size()); - return title_key; -} - -bool CIABuilder::WriteTicket() { - const auto title_id = tmd.GetTitleID(); - - Ticket ticket; - if (type == CIABuildType::Legit) { - if (!FindLegitTicket(ticket, title_id)) { - return false; - } - } else { - ticket = BuildStandardTicket(title_id); - } - title_key = GetTitleKey(ticket); - - header.tik_size = ticket.GetSize(); - - file->Seek(ticket_offset, SEEK_SET); - if (!ticket.Save(*file)) { - LOG_ERROR(Core, "Could not write ticket"); - return false; - } - return true; -} - -class CIAEncryptAndHash final : public CryptoFunc { -public: - explicit CIAEncryptAndHash(const Key::AESKey& key, const Key::AESKey& iv) { - aes.SetKeyWithIV(key.data(), key.size(), iv.data()); - } - - ~CIAEncryptAndHash() override = default; - - void ProcessData(u8* data, std::size_t size) override { - sha.Update(data, size); - aes.ProcessData(data, data, size); - } - - bool VerifyHash(const u8* hash) { - return sha.Verify(hash); - } - -private: - CryptoPP::CBC_Mode::Encryption aes; - CryptoPP::SHA256 sha; -}; - -bool CIABuilder::AddContent(u16 content_id, NCCHContainer& ncch) { - if (!ncch.Load()) { - return false; - } - - file->Seek(written, SEEK_SET); // To enforce alignment - wrapper.SetCurrent(written); - - auto& tmd_chunk = tmd.GetContentChunkByID(content_id); - - if (type == CIABuildType::Standard) { - // Decrypt the NCCH. We created a HashedFile to transparently calculate the hash as there - // is no easy way to get decrypted NCCH content otherwise. - file->SetHashEnabled(true); - { - std::lock_guard lock{abort_ncch_mutex}; - abort_ncch = &ncch; - } - const auto ret = ncch.DecryptToFile(file, wrapper.Wrap(callback)); - { - std::lock_guard lock{abort_ncch_mutex}; - abort_ncch = nullptr; - } - - if (!ret) { - return false; - } - file->GetHash(tmd_chunk.hash.data()); - file->SetHashEnabled(false); - } else { - ncch.file->Seek(0, SEEK_SET); - - // Calculate IV - Key::AESKey iv{}; - std::memcpy(iv.data(), &tmd_chunk.index, sizeof(tmd_chunk.index)); - - const bool is_encrypted = static_cast(tmd_chunk.type) & 0x01; - - // For encrypted content, the hashes are calculated before CIA/CDN encryption. - // So we have to add hash calculation to the CryptoFunc of the FileDecryptor. - // For unencrypted content, we can just use HashedFile's hashing. - std::shared_ptr crypto; - if (is_encrypted) { - crypto = std::make_shared(title_key, iv); - } else { // crypto left to be null - file->SetHashEnabled(true); - } - decryptor.SetCrypto(crypto); - if (!decryptor.CryptAndWriteFile(ncch.file, ncch.file->GetSize(), file, - wrapper.Wrap(callback))) { - - return false; - } - - // Verify the hash - bool verified{}; - if (is_encrypted) { - verified = crypto->VerifyHash(tmd_chunk.hash.data()); - } else { - verified = file->VerifyHash(tmd_chunk.hash.data()); - file->SetHashEnabled(false); - } - if (!verified) { - LOG_ERROR(Core, "Hash dismatch for content {}", content_id); - return false; - } - } - - written = Common::AlignUp(file->Tell(), CIA_ALIGNMENT); - - header.content_size = written - content_offset; - header.SetContentPresent(tmd_chunk.index); - - // DLCs do not have a meta - if (tmd_chunk.index != TMDContentIndex::Main || (tmd.GetTitleID() >> 32) == 0x0004008c) { - return true; - } - - // Load meta if the content is main - static_assert(sizeof(ncch.exheader_header.dependency_list) == sizeof(meta.dependencies), - "Dependency list should be of the same size in NCCH and CIA"); - std::memcpy(meta.dependencies.data(), &ncch.exheader_header.dependency_list, - sizeof(meta.dependencies)); - - // Note: GodMode9 has this hardcoded to 2. - meta.core_version = ncch.exheader_header.arm11_system_local_caps.core_version; - - std::vector smdh_buffer; - if (!ncch.LoadSectionExeFS("icon", smdh_buffer)) { - LOG_WARNING(Core, "Failed to load icon in ExeFS"); - return true; - } - std::memcpy(meta.icon_data.data(), smdh_buffer.data(), - std::min(meta.icon_data.size(), smdh_buffer.size())); - header.meta_size = sizeof(meta); - return true; -} - -bool CIABuilder::Finalize() { - // Write header - file->Seek(0, SEEK_SET); - if (file->WriteBytes(&header, sizeof(header)) != sizeof(header)) { - LOG_ERROR(Core, "Failed to write header"); - return false; - } - - // Write TMD - if (type == CIABuildType::Standard) { - tmd.FixHashes(); - } - file->Seek(tmd_offset, SEEK_SET); - if (!tmd.Save(*file)) { - return false; - } - - // Write meta - if (header.meta_size) { - file->Seek(written, SEEK_SET); - if (file->WriteBytes(&meta, sizeof(meta)) != sizeof(meta)) { - LOG_ERROR(Core, "Failed to write meta"); - return false; - } - } - - callback(total_size, total_size); - return true; -} - -void CIABuilder::Abort() { - if (type == CIABuildType::Standard) { // Abort NCCH decryption - std::lock_guard lock{abort_ncch_mutex}; - if (abort_ncch) { - abort_ncch->AbortDecryptToFile(); - } - } else { // Abort the decryptor - decryptor.Abort(); - } -} - -} // namespace Core +// Copyright 2020 Pengfei Zhu +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include "common/alignment.h" +#include "core/cia_builder.h" +#include "core/db/title_db.h" +#include "core/db/title_keys_bin.h" +#include "core/file_sys/certificate.h" +#include "core/file_sys/cia_common.h" +#include "core/file_sys/ticket.h" +#include "core/file_sys/title_metadata.h" +#include "core/importer.h" + +namespace Core { + +constexpr std::size_t CIA_ALIGNMENT = 0x40; + +class HashedFile : public FileUtil::IOFile { +public: + explicit HashedFile(const std::string& filename, const char openmode[], int flags = 0) + : FileUtil::IOFile(filename, openmode, flags) {} + ~HashedFile() override = default; + + void SetHashEnabled(bool enabled) { + hash_enabled = enabled; + if (enabled) { // Restart when hash is newly restarted + sha.Restart(); + } + } + + void GetHash(u8* out) { + sha.Final(out); + } + + bool VerifyHash(u8* out) { + return sha.Verify(out); + } + + std::size_t Write(const char* data, std::size_t length) override { + const std::size_t length_written = FileUtil::IOFile::Write(data, length); + if (hash_enabled) { + sha.Update(reinterpret_cast(data), length_written); + } + return length_written; + } + +private: + CryptoPP::SHA256 sha; + bool hash_enabled{}; +}; + +CIABuilder::CIABuilder(const Config& config, std::shared_ptr ticket_db_) + : ticket_db(std::move(ticket_db_)) { + if (!config.enc_title_keys_bin_path.empty()) { + enc_title_keys_bin = std::make_unique(); + 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; + +bool CIABuilder::Init(CIABuildType type_, const std::string& destination, TitleMetadata tmd_, + std::size_t total_size_, const Common::ProgressCallback& callback_) { + + type = type_; + header = {}; + meta = {}; + + if (!FileUtil::CreateFullPath(destination)) { + LOG_ERROR(Core, "Could not create {}", destination); + return false; + } + file = std::make_shared(destination, "wb"); + if (!*file) { + LOG_ERROR(Core, "Could not open file {}", destination); + return false; + } + + tmd = std::move(tmd_); + if (type == CIABuildType::Standard) { + // Remove encrypted flag from TMD chunks + for (auto& chunk : tmd.tmd_chunks) { + chunk.type &= ~0x01; + } + } + if (type == CIABuildType::Legit || type == CIABuildType::PirateLegit) { + // Check for legit TMD + if (!tmd.VerifyHashes() || !tmd.ValidateSignature()) { + LOG_ERROR(Core, "TMD is not legit"); + return false; + } + } + + header.header_size = sizeof(header); + // Header will be written in Finalize + + // Cert + cert_offset = Common::AlignUp(header.header_size, CIA_ALIGNMENT); + header.cert_size = CIA_CERT_SIZE; + if (!WriteCert()) { + LOG_ERROR(Core, "Could not write cert to file {}", destination); + return false; + } + + // Ticket + ticket_offset = Common::AlignUp(cert_offset + header.cert_size, CIA_ALIGNMENT); + if (!WriteTicket()) { + return false; + } + + // TMD will be written in Finalize (we need to set content hash, etc) + tmd_offset = Common::AlignUp(ticket_offset + header.tik_size, CIA_ALIGNMENT); + header.tmd_size = tmd.GetSize(); + + content_offset = Common::AlignUp(tmd_offset + header.tmd_size, CIA_ALIGNMENT); + header.content_size = 0; + + // Meta will be written in Finalize + header.meta_size = 0; + + // Initialize variables + written = content_offset; + total_size = total_size_; + + callback = callback_; + wrapper.total_size = total_size; + wrapper.SetCurrent(written); + + callback(written, total_size); + return true; +} + +void CIABuilder::Cleanup() { + file.reset(); +} + +bool CIABuilder::WriteCert() { + if (!Certs::IsLoaded()) { + return false; + } + + file->Seek(cert_offset, SEEK_SET); + for (const auto& cert : CIACertNames) { + if (!Certs::Get(cert).Save(*file)) { + LOG_ERROR(Core, "Failed to write cert {}", cert); + return false; + } + } + return true; +} + +bool CIABuilder::FindLegitTicket(Ticket& ticket, u64 title_id) const { + if (ticket_db && ticket_db->tickets.count(title_id)) { + ticket = ticket_db->tickets.at(title_id); + if (!ticket.ValidateSignature()) { + LOG_ERROR(Core, "Ticket in ticket.db for {:016x} is not legit", title_id); + return false; + } + return true; + } + + LOG_ERROR(Core, "Ticket for {:016x} does not exist in ticket.db", title_id); + return false; +} + +Ticket CIABuilder::BuildStandardTicket(u64 title_id) const { + Ticket ticket = BuildFakeTicket(title_id); + + // 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 + if (ticket_db && ticket_db->tickets.count(title_id)) { // ticket.db + const auto& legit_ticket = ticket_db->tickets.at(title_id); + ticket.body.common_key_index = legit_ticket.body.common_key_index; + ticket.body.title_key = legit_ticket.body.title_key; + } else if (enc_title_keys_bin && enc_title_keys_bin->count(title_id)) { // support files + const auto& entry = enc_title_keys_bin->at(title_id); + ticket.body.common_key_index = entry.common_key_index; + ticket.body.title_key = entry.title_key; + } else { + LOG_WARNING(Core, "Could not find title key for {:016x}", title_id); + } + return ticket; +} + +static Key::AESKey GetTitleKey(const Ticket& ticket) { + Key::SelectCommonKeyIndex(ticket.body.common_key_index); + if (!Key::IsNormalKeyAvailable(Key::TicketCommonKey)) { + LOG_ERROR(Core, "Ticket common key is not available"); + return {}; + } + + const auto ticket_key = Key::GetNormalKey(Key::TicketCommonKey); + Key::AESKey ctr{}; + std::memcpy(ctr.data(), &ticket.body.title_id, 8); + + CryptoPP::CBC_Mode::Decryption aes; + aes.SetKeyWithIV(ticket_key.data(), ticket_key.size(), ctr.data()); + + Key::AESKey title_key = ticket.body.title_key; + aes.ProcessData(title_key.data(), title_key.data(), title_key.size()); + return title_key; +} + +bool CIABuilder::WriteTicket() { + const auto title_id = tmd.GetTitleID(); + + Ticket ticket; + if (type == CIABuildType::Legit) { + if (!FindLegitTicket(ticket, title_id)) { + return false; + } + } else { + ticket = BuildStandardTicket(title_id); + } + title_key = GetTitleKey(ticket); + + header.tik_size = ticket.GetSize(); + + file->Seek(ticket_offset, SEEK_SET); + if (!ticket.Save(*file)) { + LOG_ERROR(Core, "Could not write ticket"); + return false; + } + return true; +} + +class CIAEncryptAndHash final : public CryptoFunc { +public: + explicit CIAEncryptAndHash(const Key::AESKey& key, const Key::AESKey& iv) { + aes.SetKeyWithIV(key.data(), key.size(), iv.data()); + } + + ~CIAEncryptAndHash() override = default; + + void ProcessData(u8* data, std::size_t size) override { + sha.Update(data, size); + aes.ProcessData(data, data, size); + } + + bool VerifyHash(const u8* hash) { + return sha.Verify(hash); + } + +private: + CryptoPP::CBC_Mode::Encryption aes; + CryptoPP::SHA256 sha; +}; + +bool CIABuilder::AddContent(u16 content_id, NCCHContainer& ncch) { + if (!ncch.Load()) { + return false; + } + + file->Seek(written, SEEK_SET); // To enforce alignment + wrapper.SetCurrent(written); + + auto& tmd_chunk = tmd.GetContentChunkByID(content_id); + + if (type == CIABuildType::Standard) { + // Decrypt the NCCH. We created a HashedFile to transparently calculate the hash as there + // is no easy way to get decrypted NCCH content otherwise. + file->SetHashEnabled(true); + { + std::lock_guard lock{abort_ncch_mutex}; + abort_ncch = &ncch; + } + const auto ret = ncch.DecryptToFile(file, wrapper.Wrap(callback)); + { + std::lock_guard lock{abort_ncch_mutex}; + abort_ncch = nullptr; + } + + if (!ret) { + return false; + } + file->GetHash(tmd_chunk.hash.data()); + file->SetHashEnabled(false); + } else { + ncch.file->Seek(0, SEEK_SET); + + // Calculate IV + Key::AESKey iv{}; + std::memcpy(iv.data(), &tmd_chunk.index, sizeof(tmd_chunk.index)); + + const bool is_encrypted = static_cast(tmd_chunk.type) & 0x01; + + // For encrypted content, the hashes are calculated before CIA/CDN encryption. + // So we have to add hash calculation to the CryptoFunc of the FileDecryptor. + // For unencrypted content, we can just use HashedFile's hashing. + std::shared_ptr crypto; + if (is_encrypted) { + crypto = std::make_shared(title_key, iv); + } else { // crypto left to be null + file->SetHashEnabled(true); + } + decryptor.SetCrypto(crypto); + if (!decryptor.CryptAndWriteFile(ncch.file, ncch.file->GetSize(), file, + wrapper.Wrap(callback))) { + + return false; + } + + // Verify the hash + bool verified{}; + if (is_encrypted) { + verified = crypto->VerifyHash(tmd_chunk.hash.data()); + } else { + verified = file->VerifyHash(tmd_chunk.hash.data()); + file->SetHashEnabled(false); + } + if (!verified) { + LOG_ERROR(Core, "Hash dismatch for content {}", content_id); + return false; + } + } + + written = Common::AlignUp(file->Tell(), CIA_ALIGNMENT); + + header.content_size = written - content_offset; + header.SetContentPresent(tmd_chunk.index); + + // DLCs do not have a meta + if (tmd_chunk.index != TMDContentIndex::Main || (tmd.GetTitleID() >> 32) == 0x0004008c) { + return true; + } + + // Load meta if the content is main + static_assert(sizeof(ncch.exheader_header.dependency_list) == sizeof(meta.dependencies), + "Dependency list should be of the same size in NCCH and CIA"); + std::memcpy(meta.dependencies.data(), &ncch.exheader_header.dependency_list, + sizeof(meta.dependencies)); + + // Note: GodMode9 has this hardcoded to 2. + meta.core_version = ncch.exheader_header.arm11_system_local_caps.core_version; + + std::vector smdh_buffer; + if (!ncch.LoadSectionExeFS("icon", smdh_buffer)) { + LOG_WARNING(Core, "Failed to load icon in ExeFS"); + return true; + } + std::memcpy(meta.icon_data.data(), smdh_buffer.data(), + std::min(meta.icon_data.size(), smdh_buffer.size())); + header.meta_size = sizeof(meta); + return true; +} + +bool CIABuilder::Finalize() { + // Write header + file->Seek(0, SEEK_SET); + if (file->WriteBytes(&header, sizeof(header)) != sizeof(header)) { + LOG_ERROR(Core, "Failed to write header"); + return false; + } + + // Write TMD + if (type == CIABuildType::Standard) { + tmd.FixHashes(); + } + file->Seek(tmd_offset, SEEK_SET); + if (!tmd.Save(*file)) { + return false; + } + + // Write meta + if (header.meta_size) { + file->Seek(written, SEEK_SET); + if (file->WriteBytes(&meta, sizeof(meta)) != sizeof(meta)) { + LOG_ERROR(Core, "Failed to write meta"); + return false; + } + } + + callback(total_size, total_size); + return true; +} + +void CIABuilder::Abort() { + if (type == CIABuildType::Standard) { // Abort NCCH decryption + std::lock_guard lock{abort_ncch_mutex}; + if (abort_ncch) { + abort_ncch->AbortDecryptToFile(); + } + } else { // Abort the decryptor + decryptor.Abort(); + } +} + +} // namespace Core diff --git a/src/core/cia_builder.h b/src/core/cia_builder.h index 9e32f76..7e52f22 100644 --- a/src/core/cia_builder.h +++ b/src/core/cia_builder.h @@ -1,137 +1,137 @@ -// Copyright 2017 Citra Emulator Project / 2020 Pengfei Zhu -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include -#include -#include -#include "common/file_util.h" -#include "common/progress_callback.h" -#include "common/swap.h" -#include "core/file_decryptor.h" -#include "core/file_sys/cia_common.h" -#include "core/file_sys/ncch_container.h" -#include "core/file_sys/title_metadata.h" -#include "core/key/key.h" - -namespace Core { - -constexpr std::size_t CIA_CONTENT_MAX_COUNT = 0x10000; -constexpr std::size_t CIA_CONTENT_BITS_SIZE = (CIA_CONTENT_MAX_COUNT / 8); -constexpr std::size_t CIA_HEADER_SIZE = 0x2020; -constexpr std::size_t CIA_CERT_SIZE = 0xA00; -constexpr std::size_t CIA_METADATA_SIZE = 0x3AC0; - -struct Config; -class EncTitleKeysBin; -class HashedFile; -class Ticket; -class TicketDB; - -class CIABuilder { -public: - explicit CIABuilder(const Config& config, std::shared_ptr ticket_db); - ~CIABuilder(); - - /** - * Initializes the building of the CIA. - * @return true on success, false otherwise - */ - bool Init(CIABuildType type, const std::string& destination, TitleMetadata tmd, - std::size_t total_size, const Common::ProgressCallback& callback); - - void Cleanup(); - - /** - * Adds an NCCH content to the CIA. - * @return true on success, false otherwise - */ - bool AddContent(u16 content_id, NCCHContainer& ncch); - - /** - * Finalizes this CIA and write remaining data. - * @return true on success, false otherwise - */ - bool Finalize(); - - /** - * Aborts the current work. In fact, only usable during AddContent. - */ - void Abort(); - -private: - struct Header { - u32_le header_size; - u16_le type; - u16_le version; - u32_le cert_size; - u32_le tik_size; - u32_le tmd_size; - u32_le meta_size; - u64_le content_size; - std::array content_present; - - bool IsContentPresent(u16 index) const { - // The content_present is a bit array which defines which content in the TMD - // is included in the CIA, so check the bit for this index and add if set. - // The bits in the content index are arranged w/ index 0 as the MSB, 7 as the LSB, etc. - return (content_present[index >> 3] & (0x80 >> (index & 7))); - } - - void SetContentPresent(u16 index) { - content_present[index >> 3] |= (0x80 >> (index & 7)); - } - }; - - static_assert(sizeof(Header) == CIA_HEADER_SIZE, "CIA Header structure size is wrong"); - - struct Metadata { - std::array dependencies; - std::array reserved; - u32_le core_version; - std::array reserved_2; - std::array icon_data; - }; - - static_assert(sizeof(Metadata) == CIA_METADATA_SIZE, "CIA Metadata structure size is wrong"); - - bool WriteCert(); - - bool FindLegitTicket(Ticket& ticket, u64 title_id) const; - Ticket BuildStandardTicket(u64 title_id) const; - bool WriteTicket(); - - // Persistent state - const std::shared_ptr ticket_db; - std::unique_ptr enc_title_keys_bin; - - // State of a single task - CIABuildType type; - - Header header{}; - Metadata meta{}; - - TitleMetadata tmd; - Key::AESKey title_key{}; - - std::size_t cert_offset{}; - std::size_t ticket_offset{}; - std::size_t tmd_offset{}; - std::size_t content_offset{}; - - std::shared_ptr file; - std::size_t written{}; // size written (with alignment) - std::size_t total_size{}; - Common::ProgressCallback callback; - Common::ProgressCallbackWrapper wrapper; - - // The NCCH to abort on - std::mutex abort_ncch_mutex; - NCCHContainer* abort_ncch{}; - - FileDecryptor decryptor; -}; - -} // namespace Core +// Copyright 2017 Citra Emulator Project / 2020 Pengfei Zhu +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include "common/file_util.h" +#include "common/progress_callback.h" +#include "common/swap.h" +#include "core/file_decryptor.h" +#include "core/file_sys/cia_common.h" +#include "core/file_sys/ncch_container.h" +#include "core/file_sys/title_metadata.h" +#include "core/key/key.h" + +namespace Core { + +constexpr std::size_t CIA_CONTENT_MAX_COUNT = 0x10000; +constexpr std::size_t CIA_CONTENT_BITS_SIZE = (CIA_CONTENT_MAX_COUNT / 8); +constexpr std::size_t CIA_HEADER_SIZE = 0x2020; +constexpr std::size_t CIA_CERT_SIZE = 0xA00; +constexpr std::size_t CIA_METADATA_SIZE = 0x3AC0; + +struct Config; +class EncTitleKeysBin; +class HashedFile; +class Ticket; +class TicketDB; + +class CIABuilder { +public: + explicit CIABuilder(const Config& config, std::shared_ptr ticket_db); + ~CIABuilder(); + + /** + * Initializes the building of the CIA. + * @return true on success, false otherwise + */ + bool Init(CIABuildType type, const std::string& destination, TitleMetadata tmd, + std::size_t total_size, const Common::ProgressCallback& callback); + + void Cleanup(); + + /** + * Adds an NCCH content to the CIA. + * @return true on success, false otherwise + */ + bool AddContent(u16 content_id, NCCHContainer& ncch); + + /** + * Finalizes this CIA and write remaining data. + * @return true on success, false otherwise + */ + bool Finalize(); + + /** + * Aborts the current work. In fact, only usable during AddContent. + */ + void Abort(); + +private: + struct Header { + u32_le header_size; + u16_le type; + u16_le version; + u32_le cert_size; + u32_le tik_size; + u32_le tmd_size; + u32_le meta_size; + u64_le content_size; + std::array content_present; + + bool IsContentPresent(u16 index) const { + // The content_present is a bit array which defines which content in the TMD + // is included in the CIA, so check the bit for this index and add if set. + // The bits in the content index are arranged w/ index 0 as the MSB, 7 as the LSB, etc. + return (content_present[index >> 3] & (0x80 >> (index & 7))); + } + + void SetContentPresent(u16 index) { + content_present[index >> 3] |= (0x80 >> (index & 7)); + } + }; + + static_assert(sizeof(Header) == CIA_HEADER_SIZE, "CIA Header structure size is wrong"); + + struct Metadata { + std::array dependencies; + std::array reserved; + u32_le core_version; + std::array reserved_2; + std::array icon_data; + }; + + static_assert(sizeof(Metadata) == CIA_METADATA_SIZE, "CIA Metadata structure size is wrong"); + + bool WriteCert(); + + bool FindLegitTicket(Ticket& ticket, u64 title_id) const; + Ticket BuildStandardTicket(u64 title_id) const; + bool WriteTicket(); + + // Persistent state + const std::shared_ptr ticket_db; + std::unique_ptr enc_title_keys_bin; + + // State of a single task + CIABuildType type; + + Header header{}; + Metadata meta{}; + + TitleMetadata tmd; + Key::AESKey title_key{}; + + std::size_t cert_offset{}; + std::size_t ticket_offset{}; + std::size_t tmd_offset{}; + std::size_t content_offset{}; + + std::shared_ptr file; + std::size_t written{}; // size written (with alignment) + std::size_t total_size{}; + Common::ProgressCallback callback; + Common::ProgressCallbackWrapper wrapper; + + // The NCCH to abort on + std::mutex abort_ncch_mutex; + NCCHContainer* abort_ncch{}; + + FileDecryptor decryptor; +}; + +} // namespace Core diff --git a/src/core/file_decryptor.h b/src/core/file_decryptor.h index 0ed4450..c5d23a9 100644 --- a/src/core/file_decryptor.h +++ b/src/core/file_decryptor.h @@ -1,93 +1,93 @@ -// Copyright 2019 threeSD Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include -#include -#include -#include -#include "common/common_types.h" -#include "common/progress_callback.h" -#include "common/thread.h" -#include "core/key/key.h" - -namespace FileUtil { -class IOFile; -} - -namespace Core { - -class CryptoFunc; - -/** - * Generalized file decryptor. - * Helper that reads, decrypts and writes data. This uses three threads to process the data - * and call progress callbacks occasionally. - */ -class FileDecryptor { -public: - explicit FileDecryptor(); - ~FileDecryptor(); - - /** - * Set up the crypto to use. - * Default / nullptr is plain copy. - */ - void SetCrypto(std::shared_ptr crypto); - - /** - * Crypts and writes a file. - * - * @param source Source file - * @param size Size to read, decrypt and write - * @param destination Destination file - * @param callback Progress callback. default for nothing. - */ - bool CryptAndWriteFile( - std::shared_ptr source, std::size_t size, - std::shared_ptr destination, - const Common::ProgressCallback& callback = [](u64, u64) {}); - - void DataReadLoop(); - void DataDecryptLoop(); - void DataWriteLoop(); - - void Abort(); - -private: - static constexpr std::size_t BufferSize = 16 * 1024; // 16 KB - - std::shared_ptr source; - std::shared_ptr destination; - std::shared_ptr crypto; - - std::size_t total_size{}; - - std::array, 3> buffers; - std::array data_read_event; - std::array data_decrypted_event; - std::array data_written_event; - - std::unique_ptr read_thread; - std::unique_ptr decrypt_thread; - std::unique_ptr write_thread; - - Common::ProgressCallback callback; - - Common::Event completion_event; - bool is_good{true}; - std::atomic_bool is_running{false}; -}; - -class CryptoFunc { -public: - virtual ~CryptoFunc(); - virtual void ProcessData(u8* data, std::size_t size) = 0; -}; - -std::shared_ptr CreateCTRCrypto(const Key::AESKey& key, const Key::AESKey& ctr, - std::size_t seek_pos = 0); - -} // namespace Core +// Copyright 2019 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include "common/common_types.h" +#include "common/progress_callback.h" +#include "common/thread.h" +#include "core/key/key.h" + +namespace FileUtil { +class IOFile; +} + +namespace Core { + +class CryptoFunc; + +/** + * Generalized file decryptor. + * Helper that reads, decrypts and writes data. This uses three threads to process the data + * and call progress callbacks occasionally. + */ +class FileDecryptor { +public: + explicit FileDecryptor(); + ~FileDecryptor(); + + /** + * Set up the crypto to use. + * Default / nullptr is plain copy. + */ + void SetCrypto(std::shared_ptr crypto); + + /** + * Crypts and writes a file. + * + * @param source Source file + * @param size Size to read, decrypt and write + * @param destination Destination file + * @param callback Progress callback. default for nothing. + */ + bool CryptAndWriteFile( + std::shared_ptr source, std::size_t size, + std::shared_ptr destination, + const Common::ProgressCallback& callback = [](u64, u64) {}); + + void DataReadLoop(); + void DataDecryptLoop(); + void DataWriteLoop(); + + void Abort(); + +private: + static constexpr std::size_t BufferSize = 16 * 1024; // 16 KB + + std::shared_ptr source; + std::shared_ptr destination; + std::shared_ptr crypto; + + std::size_t total_size{}; + + std::array, 3> buffers; + std::array data_read_event; + std::array data_decrypted_event; + std::array data_written_event; + + std::unique_ptr read_thread; + std::unique_ptr decrypt_thread; + std::unique_ptr write_thread; + + Common::ProgressCallback callback; + + Common::Event completion_event; + bool is_good{true}; + std::atomic_bool is_running{false}; +}; + +class CryptoFunc { +public: + virtual ~CryptoFunc(); + virtual void ProcessData(u8* data, std::size_t size) = 0; +}; + +std::shared_ptr CreateCTRCrypto(const Key::AESKey& key, const Key::AESKey& ctr, + std::size_t seek_pos = 0); + +} // namespace Core diff --git a/src/core/file_sys/ncch_container.cpp b/src/core/file_sys/ncch_container.cpp index 85ef819..2aa9951 100644 --- a/src/core/file_sys/ncch_container.cpp +++ b/src/core/file_sys/ncch_container.cpp @@ -1,616 +1,616 @@ -// Copyright 2017 Citra Emulator Project / 2019 threeSD Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include -#include -#include -#include -#include -#include -#include -#include "common/alignment.h" -#include "common/assert.h" -#include "common/common_funcs.h" -#include "common/common_types.h" -#include "common/logging/log.h" -#include "core/db/seed_db.h" -#include "core/file_sys/data/data_container.h" -#include "core/file_sys/ncch_container.h" -#include "core/key/key.h" - -namespace Core { - -static const int kMaxSections = 8; ///< Maximum number of sections (files) in an ExeFs -static const int kBlockSize = 0x200; ///< Size of ExeFS blocks (in bytes) - -NCCHContainer::NCCHContainer(std::shared_ptr file_) : file(std::move(file_)) {} - -bool NCCHContainer::OpenFile(std::shared_ptr file_) { - file = std::move(file_); - - if (!file->IsOpen()) { - LOG_WARNING(Service_FS, "Failed to open"); - return false; - } - - LOG_DEBUG(Service_FS, "Opened"); - return true; -} - -bool NCCHContainer::Load() { - if (is_loaded) - return true; - - if (!file->IsOpen()) { - LOG_WARNING(Service_FS, "Failed to open"); - return false; - } - - // Reset read pointer in case this file has been read before. - file->Seek(0, SEEK_SET); - - if (file->ReadBytes(&ncch_header, sizeof(NCCH_Header)) != sizeof(NCCH_Header)) { - LOG_ERROR(Service_FS, "Could not read from file"); - return false; - } - - // Verify we are loading the correct file type... - if (MakeMagic('N', 'C', 'C', 'H') != ncch_header.magic) { - LOG_ERROR(Service_FS, "Invalid magic, file may be corrupted"); - return false; - } - - bool failed_to_decrypt = false; - if (!ncch_header.no_crypto) { - is_encrypted = true; - - // Find primary and secondary keys - if (ncch_header.fixed_key) { - LOG_DEBUG(Service_FS, "Fixed-key crypto"); - primary_key.fill(0); - secondary_key.fill(0); - } else { - std::array key_y_primary, key_y_secondary; - - std::copy(ncch_header.signature, ncch_header.signature + key_y_primary.size(), - key_y_primary.begin()); - - if (!ncch_header.seed_crypto) { - key_y_secondary = key_y_primary; - } else { - auto opt{Seeds::GetSeed(ncch_header.program_id)}; - if (!opt.has_value()) { - LOG_ERROR(Service_FS, "Seed for program {:016X} not found", - ncch_header.program_id); - failed_to_decrypt = true; - } else { - auto seed{*opt}; - std::array input; - std::memcpy(input.data(), key_y_primary.data(), key_y_primary.size()); - std::memcpy(input.data() + key_y_primary.size(), seed.data(), seed.size()); - CryptoPP::SHA256 sha; - std::array hash; - sha.CalculateDigest(hash.data(), input.data(), input.size()); - std::memcpy(key_y_secondary.data(), hash.data(), key_y_secondary.size()); - } - } - - Key::SetKeyY(Key::NCCHSecure1, key_y_primary); - if (!Key::IsNormalKeyAvailable(Key::NCCHSecure1)) { - LOG_ERROR(Service_FS, "Secure1 KeyX missing"); - failed_to_decrypt = true; - } - primary_key = Key::GetNormalKey(Key::NCCHSecure1); - - const auto SetSecondaryKey = [this, &failed_to_decrypt, - &key_y_secondary](Key::KeySlotID slot) { - Key::SetKeyY(slot, key_y_secondary); - if (!Key::IsNormalKeyAvailable(slot)) { - LOG_ERROR(Service_FS, "{:#04X} KeyX missing", slot); - failed_to_decrypt = true; - } - secondary_key = Key::GetNormalKey(slot); - }; - - switch (ncch_header.secondary_key_slot) { - case 0: - LOG_DEBUG(Service_FS, "Secure1 crypto"); - SetSecondaryKey(Key::NCCHSecure1); - break; - case 1: - LOG_DEBUG(Service_FS, "Secure2 crypto"); - SetSecondaryKey(Key::NCCHSecure2); - break; - case 10: - LOG_DEBUG(Service_FS, "Secure3 crypto"); - SetSecondaryKey(Key::NCCHSecure3); - break; - case 11: - LOG_DEBUG(Service_FS, "Secure4 crypto"); - SetSecondaryKey(Key::NCCHSecure4); - break; - } - } - - // Find CTR for each section - // Written with reference to - // https://github.com/d0k3/GodMode9/blob/99af6a73be48fa7872649aaa7456136da0df7938/arm9/source/game/ncch.c#L34-L52 - if (ncch_header.version == 0 || ncch_header.version == 2) { - LOG_DEBUG(Loader, "NCCH version 0/2"); - // In this version, CTR for each section is a magic number prefixed by partition ID - // (reverse order) - std::reverse_copy(ncch_header.partition_id, ncch_header.partition_id + 8, - exheader_ctr.begin()); - exefs_ctr = romfs_ctr = exheader_ctr; - exheader_ctr[8] = 1; - exefs_ctr[8] = 2; - romfs_ctr[8] = 3; - } else if (ncch_header.version == 1) { - LOG_DEBUG(Loader, "NCCH version 1"); - // In this version, CTR for each section is the section offset prefixed by partition - // ID, as if the entire NCCH image is encrypted using a single CTR stream. - std::copy(ncch_header.partition_id, ncch_header.partition_id + 8, exheader_ctr.begin()); - exefs_ctr = romfs_ctr = exheader_ctr; - auto u32ToBEArray = [](u32 value) -> std::array { - return std::array{ - static_cast(value >> 24), - static_cast((value >> 16) & 0xFF), - static_cast((value >> 8) & 0xFF), - static_cast(value & 0xFF), - }; - }; - auto offset_exheader = u32ToBEArray(0x200); // exheader offset - auto offset_exefs = u32ToBEArray(ncch_header.exefs_offset * kBlockSize); - auto offset_romfs = u32ToBEArray(ncch_header.romfs_offset * kBlockSize); - std::copy(offset_exheader.begin(), offset_exheader.end(), exheader_ctr.begin() + 12); - std::copy(offset_exefs.begin(), offset_exefs.end(), exefs_ctr.begin() + 12); - std::copy(offset_romfs.begin(), offset_romfs.end(), romfs_ctr.begin() + 12); - } else { - LOG_ERROR(Service_FS, "Unknown NCCH version {}", ncch_header.version); - failed_to_decrypt = true; - } - } else { - LOG_DEBUG(Service_FS, "No crypto"); - is_encrypted = false; - } - - // System archives and DLC don't have an extended header but have RomFS - if (ncch_header.extended_header_size) { - if (file->ReadBytes(&exheader_header, sizeof(exheader_header)) != sizeof(exheader_header)) { - LOG_ERROR(Service_FS, "Could not read exheader from file"); - return false; - } - - if (is_encrypted) { - // This ID check is masked to low 32-bit as a toleration to ill-formed ROM created - // by merging games and its updates. - if ((exheader_header.system_info.jump_id & 0xFFFFFFFF) == - (ncch_header.program_id & 0xFFFFFFFF)) { - LOG_WARNING(Service_FS, "NCCH is marked as encrypted but with decrypted " - "exheader. Force no crypto scheme."); - is_encrypted = false; - } else { - if (failed_to_decrypt) { - LOG_ERROR(Service_FS, "Failed to decrypt"); - return false; - } - CryptoPP::byte* data = reinterpret_cast(&exheader_header); - CryptoPP::CTR_Mode::Decryption( - primary_key.data(), primary_key.size(), exheader_ctr.data()) - .ProcessData(data, data, sizeof(exheader_header)); - } - } - - u32 entry_point = exheader_header.codeset_info.text.address; - u32 code_size = exheader_header.codeset_info.text.code_size; - u32 stack_size = exheader_header.codeset_info.stack_size; - u32 bss_size = exheader_header.codeset_info.bss_size; - u32 core_version = exheader_header.arm11_system_local_caps.core_version; - u8 priority = exheader_header.arm11_system_local_caps.priority; - u8 resource_limit_category = - exheader_header.arm11_system_local_caps.resource_limit_category; - - LOG_DEBUG(Service_FS, "Name: {}", exheader_header.codeset_info.name); - LOG_DEBUG(Service_FS, "Program ID: {:016X}", ncch_header.program_id); - LOG_DEBUG(Service_FS, "Entry point: 0x{:08X}", entry_point); - LOG_DEBUG(Service_FS, "Code size: 0x{:08X}", code_size); - LOG_DEBUG(Service_FS, "Stack size: 0x{:08X}", stack_size); - LOG_DEBUG(Service_FS, "Bss size: 0x{:08X}", bss_size); - LOG_DEBUG(Service_FS, "Core version: {}", core_version); - LOG_DEBUG(Service_FS, "Thread priority: 0x{:X}", priority); - LOG_DEBUG(Service_FS, "Resource limit category: {}", resource_limit_category); - LOG_DEBUG(Service_FS, "System Mode: {}", - static_cast(exheader_header.arm11_system_local_caps.system_mode)); - - has_exheader = true; - } - - // DLC can have an ExeFS and a RomFS but no extended header - if (ncch_header.exefs_size) { - exefs_offset = ncch_header.exefs_offset * kBlockSize; - u32 exefs_size = ncch_header.exefs_size * kBlockSize; - - LOG_DEBUG(Service_FS, "ExeFS offset: 0x{:08X}", exefs_offset); - LOG_DEBUG(Service_FS, "ExeFS size: 0x{:08X}", exefs_size); - file->Seek(exefs_offset, SEEK_SET); - if (file->ReadBytes(&exefs_header, sizeof(ExeFs_Header)) != sizeof(ExeFs_Header)) { - LOG_ERROR(Service_FS, "Could not read ExeFS header from file"); - return false; - } - - if (is_encrypted) { - CryptoPP::byte* data = reinterpret_cast(&exefs_header); - CryptoPP::CTR_Mode::Decryption(primary_key.data(), primary_key.size(), - exefs_ctr.data()) - .ProcessData(data, data, sizeof(exefs_header)); - } - - exefs_file = file; - has_exefs = true; - } - - if (ncch_header.romfs_offset != 0 && ncch_header.romfs_size != 0) - has_romfs = true; - - is_loaded = true; - return true; -} - -bool NCCHContainer::LoadSectionExeFS(const char* name, std::vector& buffer) { - if (!Load()) { - return false; - } - - if (!exefs_file || !exefs_file->IsOpen()) { - LOG_ERROR(Service_FS, "NCCH does not have ExeFS"); - return false; - } - - LOG_DEBUG(Service_FS, "{} sections:", kMaxSections); - // Iterate through the ExeFs archive until we find a section with the specified name... - for (unsigned section_number = 0; section_number < kMaxSections; section_number++) { - const auto& section = exefs_header.section[section_number]; - - if (strcmp(section.name, name) == 0) { - LOG_DEBUG(Service_FS, "{} - offset: 0x{:08X}, size: 0x{:08X}, name: {}", section_number, - section.offset, section.size, section.name); - - s64 section_offset = (section.offset + exefs_offset + sizeof(ExeFs_Header)); - exefs_file->Seek(section_offset, SEEK_SET); - - CryptoPP::CTR_Mode::Decryption dec(primary_key.data(), - primary_key.size(), exefs_ctr.data()); - dec.Seek(section.offset + sizeof(ExeFs_Header)); - - buffer.resize(section.size); - if (exefs_file->ReadBytes(&buffer[0], section.size) != section.size) - return false; - if (is_encrypted) { - dec.ProcessData(&buffer[0], &buffer[0], section.size); - } - - return true; - } - } - LOG_ERROR(Service_FS, "Section {} not found", name); - return false; -} - -bool NCCHContainer::ReadProgramId(u64_le& program_id) { - if (!Load()) { - return false; - } - - program_id = ncch_header.program_id; - return true; -} - -bool NCCHContainer::ReadExtdataId(u64& extdata_id) { - if (!Load()) { - return false; - } - - if (!has_exheader) { - LOG_ERROR(Service_FS, "NCCH does not have ExHeader"); - return false; - } - - if (exheader_header.arm11_system_local_caps.storage_info.other_attributes >> 1) { - // Using extended save data access - // There would be multiple possible extdata IDs in this case. The best we can do for now is - // guessing that the first one would be the main save. - const std::array extdata_ids{{ - exheader_header.arm11_system_local_caps.storage_info.extdata_id0.Value(), - exheader_header.arm11_system_local_caps.storage_info.extdata_id1.Value(), - exheader_header.arm11_system_local_caps.storage_info.extdata_id2.Value(), - exheader_header.arm11_system_local_caps.storage_info.extdata_id3.Value(), - exheader_header.arm11_system_local_caps.storage_info.extdata_id4.Value(), - exheader_header.arm11_system_local_caps.storage_info.extdata_id5.Value(), - }}; - for (u64 id : extdata_ids) { - if (id) { - // Found a non-zero ID, use it - extdata_id = id; - return true; - } - } - - LOG_INFO(Service_FS, "Title does not have extdata ID"); - return false; - } - - extdata_id = exheader_header.arm11_system_local_caps.storage_info.ext_save_data_id; - return true; -} - -bool NCCHContainer::HasExeFS() { - if (!Load()) { - return false; - } - - return has_exefs; -} - -bool NCCHContainer::HasExHeader() { - if (!Load()) { - return false; - } - - return has_exheader; -} - -bool NCCHContainer::ReadCodesetName(std::string& name) { - if (!Load()) { - return false; - } - - if (!has_exheader) { - LOG_ERROR(Service_FS, "NCCH does not have ExHeader"); - return false; - } - - std::array name_data{}; - std::memcpy(name_data.data(), exheader_header.codeset_info.name, 8); - name = name_data.data(); - return true; -} - -bool NCCHContainer::ReadProductCode(std::string& product_code) { - if (!Load()) { - return false; - } - - std::array data{}; - std::memcpy(data.data(), ncch_header.product_code, 16); - product_code = data.data(); - return true; -} - -bool NCCHContainer::ReadEncryptionType(EncryptionType& encryption) { - if (!Load()) { - return false; - } - - if (!is_encrypted) { - encryption = EncryptionType::None; - } else if (ncch_header.fixed_key) { - encryption = EncryptionType::FixedKey; - } else { - switch (ncch_header.secondary_key_slot) { - case 0: - encryption = EncryptionType::NCCHSecure1; - break; - case 1: - encryption = EncryptionType::NCCHSecure2; - break; - case 10: - encryption = EncryptionType::NCCHSecure3; - break; - case 11: - encryption = EncryptionType::NCCHSecure4; - break; - default: - LOG_ERROR(Service_FS, "Unknown encryption type {:X}!", ncch_header.secondary_key_slot); - return false; - } - } - - return true; -} - -bool NCCHContainer::ReadSeedCrypto(bool& used) { - if (!Load()) { - return false; - } - - used = ncch_header.seed_crypto; - return true; -} - -bool NCCHContainer::DecryptToFile(std::shared_ptr dest_file, - const Common::ProgressCallback& callback) { - if (!Load()) { - return false; - } - - if (!*dest_file) { - LOG_ERROR(Core, "File is not open"); - return false; - } - - if (!is_encrypted) { - // Simply copy everything. FileDecryptor is used for progress reporting - file->Seek(0, SEEK_SET); - - const auto size = file->GetSize(); - - decryptor.SetCrypto(nullptr); - return decryptor.CryptAndWriteFile(file, size, dest_file, callback); - } - - const auto total_size = file->GetSize(); - std::size_t written{}; - - // Write NCCH header - NCCH_Header modified_header = ncch_header; - - // Set flags (equivalent to GodMode9 behaviour) - modified_header.secondary_key_slot = 0; - modified_header.fixed_key.Assign(0); - modified_header.no_crypto.Assign(1); - modified_header.seed_crypto.Assign(0); - - if (dest_file->WriteBytes(&modified_header, sizeof(modified_header)) != - sizeof(modified_header)) { - LOG_ERROR(Core, "Could not write NCCH header to file"); - return false; - } - written += sizeof(NCCH_Header); - - // Write Exheader - if (has_exheader) { - if (dest_file->WriteBytes(&exheader_header, sizeof(exheader_header)) != - sizeof(exheader_header)) { - LOG_ERROR(Core, "Could not write Exheader to file"); - return false; - } - written += sizeof(ExHeader_Header); - } - - Common::ProgressCallbackWrapper wrapper{total_size}; - const auto Write = [&](std::string_view name, std::size_t offset, std::size_t size, - bool decrypt = false, const Key::AESKey& key = {}, - const Key::AESKey& ctr = {}, std::size_t aes_seek_pos = 0) { - if (offset == 0 || size == 0) { - return true; - } - - if (aborted.exchange(false)) { - return false; - } - ASSERT_MSG(written <= offset, "Offsets are not in increasing order"); - - // Zero out the gap manually to ensure correct hashes when used with CIAs, etc. - const std::array zeroes{}; - std::size_t zeroes_left = offset - written; - while (zeroes_left > 0) { - const auto to_write = std::min(zeroes.size(), zeroes_left); - if (dest_file->WriteBytes(zeroes.data(), to_write) != to_write) { - LOG_ERROR(Core, "Could not write zeroes before {}", name); - return false; - } - zeroes_left -= to_write; - } - - file->Seek(offset, SEEK_SET); - - if (aborted.exchange(false)) { - return false; - } - - written = offset; - wrapper.SetCurrent(written); - - decryptor.SetCrypto(decrypt ? CreateCTRCrypto(key, ctr, aes_seek_pos) : nullptr); - if (!decryptor.CryptAndWriteFile(file, size, dest_file, wrapper.Wrap(callback))) { - LOG_ERROR(Core, "Could not write {}", name); - return false; - } - written = offset + size; - return true; - }; - - if (!Write("logo", ncch_header.logo_region_offset * 0x200, - ncch_header.logo_region_size * 0x200)) { - return false; - } - - if (!Write("plain region", ncch_header.plain_region_offset * 0x200, - ncch_header.plain_region_size * 0x200)) { - return false; - } - - // Write ExeFS header - if (has_exefs) { - if (dest_file->WriteBytes(&exefs_header, sizeof(exefs_header)) != sizeof(exefs_header)) { - LOG_ERROR(Core, "Could not write ExeFS header to file"); - return false; - } - written += sizeof(ExeFs_Header); - - for (unsigned section_number = 0; section_number < kMaxSections; section_number++) { - const auto& section = exefs_header.section[section_number]; - if (section.offset == 0 && section.size == 0) { // not used - continue; - } - - Key::AESKey key; - if (strcmp(section.name, "icon") == 0 || strcmp(section.name, "banner") == 0) { - key = primary_key; - } else { - key = secondary_key; - } - - // Plus 1 for the ExeFS header - if (!Write(section.name, section.offset + (ncch_header.exefs_offset + 1) * 0x200, - section.size, true, key, exefs_ctr, section.offset + sizeof(exefs_header))) { - return false; - } - } - } - - if (has_romfs && !Write("romfs", ncch_header.romfs_offset * 0x200, - ncch_header.romfs_size * 0x200, true, secondary_key, romfs_ctr)) { - return false; - } - if (written < total_size) { - LOG_WARNING(Core, "Data after {} ignored", written); - } - - callback(total_size, total_size); - return true; -} - -void NCCHContainer::AbortDecryptToFile() { - aborted = true; - decryptor.Abort(); -} - -#pragma pack(push, 1) -struct RomFSIVFCHeader { - u32_le magic; - u32_le version; - u32_le master_hash_size; - std::array levels; - INSERT_PADDING_BYTES(0xC); -}; -static_assert(sizeof(RomFSIVFCHeader) == 0x60, "Size of RomFSIVFCHeader is incorrect"); -#pragma pack(pop) - -std::vector LoadSharedRomFS(const std::vector& data) { - NCCH_Header header; - ASSERT_MSG(data.size() >= sizeof(header), "NCCH size is too small"); - std::memcpy(&header, data.data(), sizeof(header)); - - const std::size_t offset = header.romfs_offset * 0x200; // 0x200: Media unit - RomFSIVFCHeader ivfc; - ASSERT_MSG(data.size() >= offset + sizeof(ivfc), "NCCH size is too small"); - std::memcpy(&ivfc, data.data() + offset, sizeof(ivfc)); - - ASSERT_MSG(ivfc.magic == MakeMagic('I', 'V', 'F', 'C'), "IVFC magic is incorrect"); - ASSERT_MSG(ivfc.version == 0x10000, "IVFC version is incorrect"); - - std::vector result(ivfc.levels[2].size); - - // Calculation from ctrtool - const std::size_t data_offset = - offset + Common::AlignUp(sizeof(ivfc) + ivfc.master_hash_size, - std::pow(2, ivfc.levels[2].block_size)); - ASSERT_MSG(data.size() >= data_offset + ivfc.levels[2].size); - std::memcpy(result.data(), data.data() + data_offset, ivfc.levels[2].size); - - return result; -} - -} // namespace Core +// Copyright 2017 Citra Emulator Project / 2019 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include +#include +#include "common/alignment.h" +#include "common/assert.h" +#include "common/common_funcs.h" +#include "common/common_types.h" +#include "common/logging/log.h" +#include "core/db/seed_db.h" +#include "core/file_sys/data/data_container.h" +#include "core/file_sys/ncch_container.h" +#include "core/key/key.h" + +namespace Core { + +static const int kMaxSections = 8; ///< Maximum number of sections (files) in an ExeFs +static const int kBlockSize = 0x200; ///< Size of ExeFS blocks (in bytes) + +NCCHContainer::NCCHContainer(std::shared_ptr file_) : file(std::move(file_)) {} + +bool NCCHContainer::OpenFile(std::shared_ptr file_) { + file = std::move(file_); + + if (!file->IsOpen()) { + LOG_WARNING(Service_FS, "Failed to open"); + return false; + } + + LOG_DEBUG(Service_FS, "Opened"); + return true; +} + +bool NCCHContainer::Load() { + if (is_loaded) + return true; + + if (!file->IsOpen()) { + LOG_WARNING(Service_FS, "Failed to open"); + return false; + } + + // Reset read pointer in case this file has been read before. + file->Seek(0, SEEK_SET); + + if (file->ReadBytes(&ncch_header, sizeof(NCCH_Header)) != sizeof(NCCH_Header)) { + LOG_ERROR(Service_FS, "Could not read from file"); + return false; + } + + // Verify we are loading the correct file type... + if (MakeMagic('N', 'C', 'C', 'H') != ncch_header.magic) { + LOG_ERROR(Service_FS, "Invalid magic, file may be corrupted"); + return false; + } + + bool failed_to_decrypt = false; + if (!ncch_header.no_crypto) { + is_encrypted = true; + + // Find primary and secondary keys + if (ncch_header.fixed_key) { + LOG_DEBUG(Service_FS, "Fixed-key crypto"); + primary_key.fill(0); + secondary_key.fill(0); + } else { + std::array key_y_primary, key_y_secondary; + + std::copy(ncch_header.signature, ncch_header.signature + key_y_primary.size(), + key_y_primary.begin()); + + if (!ncch_header.seed_crypto) { + key_y_secondary = key_y_primary; + } else { + auto opt{Seeds::GetSeed(ncch_header.program_id)}; + if (!opt.has_value()) { + LOG_ERROR(Service_FS, "Seed for program {:016X} not found", + ncch_header.program_id); + failed_to_decrypt = true; + } else { + auto seed{*opt}; + std::array input; + std::memcpy(input.data(), key_y_primary.data(), key_y_primary.size()); + std::memcpy(input.data() + key_y_primary.size(), seed.data(), seed.size()); + CryptoPP::SHA256 sha; + std::array hash; + sha.CalculateDigest(hash.data(), input.data(), input.size()); + std::memcpy(key_y_secondary.data(), hash.data(), key_y_secondary.size()); + } + } + + Key::SetKeyY(Key::NCCHSecure1, key_y_primary); + if (!Key::IsNormalKeyAvailable(Key::NCCHSecure1)) { + LOG_ERROR(Service_FS, "Secure1 KeyX missing"); + failed_to_decrypt = true; + } + primary_key = Key::GetNormalKey(Key::NCCHSecure1); + + const auto SetSecondaryKey = [this, &failed_to_decrypt, + &key_y_secondary](Key::KeySlotID slot) { + Key::SetKeyY(slot, key_y_secondary); + if (!Key::IsNormalKeyAvailable(slot)) { + LOG_ERROR(Service_FS, "{:#04X} KeyX missing", slot); + failed_to_decrypt = true; + } + secondary_key = Key::GetNormalKey(slot); + }; + + switch (ncch_header.secondary_key_slot) { + case 0: + LOG_DEBUG(Service_FS, "Secure1 crypto"); + SetSecondaryKey(Key::NCCHSecure1); + break; + case 1: + LOG_DEBUG(Service_FS, "Secure2 crypto"); + SetSecondaryKey(Key::NCCHSecure2); + break; + case 10: + LOG_DEBUG(Service_FS, "Secure3 crypto"); + SetSecondaryKey(Key::NCCHSecure3); + break; + case 11: + LOG_DEBUG(Service_FS, "Secure4 crypto"); + SetSecondaryKey(Key::NCCHSecure4); + break; + } + } + + // Find CTR for each section + // Written with reference to + // https://github.com/d0k3/GodMode9/blob/99af6a73be48fa7872649aaa7456136da0df7938/arm9/source/game/ncch.c#L34-L52 + if (ncch_header.version == 0 || ncch_header.version == 2) { + LOG_DEBUG(Loader, "NCCH version 0/2"); + // In this version, CTR for each section is a magic number prefixed by partition ID + // (reverse order) + std::reverse_copy(ncch_header.partition_id, ncch_header.partition_id + 8, + exheader_ctr.begin()); + exefs_ctr = romfs_ctr = exheader_ctr; + exheader_ctr[8] = 1; + exefs_ctr[8] = 2; + romfs_ctr[8] = 3; + } else if (ncch_header.version == 1) { + LOG_DEBUG(Loader, "NCCH version 1"); + // In this version, CTR for each section is the section offset prefixed by partition + // ID, as if the entire NCCH image is encrypted using a single CTR stream. + std::copy(ncch_header.partition_id, ncch_header.partition_id + 8, exheader_ctr.begin()); + exefs_ctr = romfs_ctr = exheader_ctr; + auto u32ToBEArray = [](u32 value) -> std::array { + return std::array{ + static_cast(value >> 24), + static_cast((value >> 16) & 0xFF), + static_cast((value >> 8) & 0xFF), + static_cast(value & 0xFF), + }; + }; + auto offset_exheader = u32ToBEArray(0x200); // exheader offset + auto offset_exefs = u32ToBEArray(ncch_header.exefs_offset * kBlockSize); + auto offset_romfs = u32ToBEArray(ncch_header.romfs_offset * kBlockSize); + std::copy(offset_exheader.begin(), offset_exheader.end(), exheader_ctr.begin() + 12); + std::copy(offset_exefs.begin(), offset_exefs.end(), exefs_ctr.begin() + 12); + std::copy(offset_romfs.begin(), offset_romfs.end(), romfs_ctr.begin() + 12); + } else { + LOG_ERROR(Service_FS, "Unknown NCCH version {}", ncch_header.version); + failed_to_decrypt = true; + } + } else { + LOG_DEBUG(Service_FS, "No crypto"); + is_encrypted = false; + } + + // System archives and DLC don't have an extended header but have RomFS + if (ncch_header.extended_header_size) { + if (file->ReadBytes(&exheader_header, sizeof(exheader_header)) != sizeof(exheader_header)) { + LOG_ERROR(Service_FS, "Could not read exheader from file"); + return false; + } + + if (is_encrypted) { + // This ID check is masked to low 32-bit as a toleration to ill-formed ROM created + // by merging games and its updates. + if ((exheader_header.system_info.jump_id & 0xFFFFFFFF) == + (ncch_header.program_id & 0xFFFFFFFF)) { + LOG_WARNING(Service_FS, "NCCH is marked as encrypted but with decrypted " + "exheader. Force no crypto scheme."); + is_encrypted = false; + } else { + if (failed_to_decrypt) { + LOG_ERROR(Service_FS, "Failed to decrypt"); + return false; + } + CryptoPP::byte* data = reinterpret_cast(&exheader_header); + CryptoPP::CTR_Mode::Decryption( + primary_key.data(), primary_key.size(), exheader_ctr.data()) + .ProcessData(data, data, sizeof(exheader_header)); + } + } + + u32 entry_point = exheader_header.codeset_info.text.address; + u32 code_size = exheader_header.codeset_info.text.code_size; + u32 stack_size = exheader_header.codeset_info.stack_size; + u32 bss_size = exheader_header.codeset_info.bss_size; + u32 core_version = exheader_header.arm11_system_local_caps.core_version; + u8 priority = exheader_header.arm11_system_local_caps.priority; + u8 resource_limit_category = + exheader_header.arm11_system_local_caps.resource_limit_category; + + LOG_DEBUG(Service_FS, "Name: {}", exheader_header.codeset_info.name); + LOG_DEBUG(Service_FS, "Program ID: {:016X}", ncch_header.program_id); + LOG_DEBUG(Service_FS, "Entry point: 0x{:08X}", entry_point); + LOG_DEBUG(Service_FS, "Code size: 0x{:08X}", code_size); + LOG_DEBUG(Service_FS, "Stack size: 0x{:08X}", stack_size); + LOG_DEBUG(Service_FS, "Bss size: 0x{:08X}", bss_size); + LOG_DEBUG(Service_FS, "Core version: {}", core_version); + LOG_DEBUG(Service_FS, "Thread priority: 0x{:X}", priority); + LOG_DEBUG(Service_FS, "Resource limit category: {}", resource_limit_category); + LOG_DEBUG(Service_FS, "System Mode: {}", + static_cast(exheader_header.arm11_system_local_caps.system_mode)); + + has_exheader = true; + } + + // DLC can have an ExeFS and a RomFS but no extended header + if (ncch_header.exefs_size) { + exefs_offset = ncch_header.exefs_offset * kBlockSize; + u32 exefs_size = ncch_header.exefs_size * kBlockSize; + + LOG_DEBUG(Service_FS, "ExeFS offset: 0x{:08X}", exefs_offset); + LOG_DEBUG(Service_FS, "ExeFS size: 0x{:08X}", exefs_size); + file->Seek(exefs_offset, SEEK_SET); + if (file->ReadBytes(&exefs_header, sizeof(ExeFs_Header)) != sizeof(ExeFs_Header)) { + LOG_ERROR(Service_FS, "Could not read ExeFS header from file"); + return false; + } + + if (is_encrypted) { + CryptoPP::byte* data = reinterpret_cast(&exefs_header); + CryptoPP::CTR_Mode::Decryption(primary_key.data(), primary_key.size(), + exefs_ctr.data()) + .ProcessData(data, data, sizeof(exefs_header)); + } + + exefs_file = file; + has_exefs = true; + } + + if (ncch_header.romfs_offset != 0 && ncch_header.romfs_size != 0) + has_romfs = true; + + is_loaded = true; + return true; +} + +bool NCCHContainer::LoadSectionExeFS(const char* name, std::vector& buffer) { + if (!Load()) { + return false; + } + + if (!exefs_file || !exefs_file->IsOpen()) { + LOG_ERROR(Service_FS, "NCCH does not have ExeFS"); + return false; + } + + LOG_DEBUG(Service_FS, "{} sections:", kMaxSections); + // Iterate through the ExeFs archive until we find a section with the specified name... + for (unsigned section_number = 0; section_number < kMaxSections; section_number++) { + const auto& section = exefs_header.section[section_number]; + + if (strcmp(section.name, name) == 0) { + LOG_DEBUG(Service_FS, "{} - offset: 0x{:08X}, size: 0x{:08X}, name: {}", section_number, + section.offset, section.size, section.name); + + s64 section_offset = (section.offset + exefs_offset + sizeof(ExeFs_Header)); + exefs_file->Seek(section_offset, SEEK_SET); + + CryptoPP::CTR_Mode::Decryption dec(primary_key.data(), + primary_key.size(), exefs_ctr.data()); + dec.Seek(section.offset + sizeof(ExeFs_Header)); + + buffer.resize(section.size); + if (exefs_file->ReadBytes(&buffer[0], section.size) != section.size) + return false; + if (is_encrypted) { + dec.ProcessData(&buffer[0], &buffer[0], section.size); + } + + return true; + } + } + LOG_ERROR(Service_FS, "Section {} not found", name); + return false; +} + +bool NCCHContainer::ReadProgramId(u64_le& program_id) { + if (!Load()) { + return false; + } + + program_id = ncch_header.program_id; + return true; +} + +bool NCCHContainer::ReadExtdataId(u64& extdata_id) { + if (!Load()) { + return false; + } + + if (!has_exheader) { + LOG_ERROR(Service_FS, "NCCH does not have ExHeader"); + return false; + } + + if (exheader_header.arm11_system_local_caps.storage_info.other_attributes >> 1) { + // Using extended save data access + // There would be multiple possible extdata IDs in this case. The best we can do for now is + // guessing that the first one would be the main save. + const std::array extdata_ids{{ + exheader_header.arm11_system_local_caps.storage_info.extdata_id0.Value(), + exheader_header.arm11_system_local_caps.storage_info.extdata_id1.Value(), + exheader_header.arm11_system_local_caps.storage_info.extdata_id2.Value(), + exheader_header.arm11_system_local_caps.storage_info.extdata_id3.Value(), + exheader_header.arm11_system_local_caps.storage_info.extdata_id4.Value(), + exheader_header.arm11_system_local_caps.storage_info.extdata_id5.Value(), + }}; + for (u64 id : extdata_ids) { + if (id) { + // Found a non-zero ID, use it + extdata_id = id; + return true; + } + } + + LOG_INFO(Service_FS, "Title does not have extdata ID"); + return false; + } + + extdata_id = exheader_header.arm11_system_local_caps.storage_info.ext_save_data_id; + return true; +} + +bool NCCHContainer::HasExeFS() { + if (!Load()) { + return false; + } + + return has_exefs; +} + +bool NCCHContainer::HasExHeader() { + if (!Load()) { + return false; + } + + return has_exheader; +} + +bool NCCHContainer::ReadCodesetName(std::string& name) { + if (!Load()) { + return false; + } + + if (!has_exheader) { + LOG_ERROR(Service_FS, "NCCH does not have ExHeader"); + return false; + } + + std::array name_data{}; + std::memcpy(name_data.data(), exheader_header.codeset_info.name, 8); + name = name_data.data(); + return true; +} + +bool NCCHContainer::ReadProductCode(std::string& product_code) { + if (!Load()) { + return false; + } + + std::array data{}; + std::memcpy(data.data(), ncch_header.product_code, 16); + product_code = data.data(); + return true; +} + +bool NCCHContainer::ReadEncryptionType(EncryptionType& encryption) { + if (!Load()) { + return false; + } + + if (!is_encrypted) { + encryption = EncryptionType::None; + } else if (ncch_header.fixed_key) { + encryption = EncryptionType::FixedKey; + } else { + switch (ncch_header.secondary_key_slot) { + case 0: + encryption = EncryptionType::NCCHSecure1; + break; + case 1: + encryption = EncryptionType::NCCHSecure2; + break; + case 10: + encryption = EncryptionType::NCCHSecure3; + break; + case 11: + encryption = EncryptionType::NCCHSecure4; + break; + default: + LOG_ERROR(Service_FS, "Unknown encryption type {:X}!", ncch_header.secondary_key_slot); + return false; + } + } + + return true; +} + +bool NCCHContainer::ReadSeedCrypto(bool& used) { + if (!Load()) { + return false; + } + + used = ncch_header.seed_crypto; + return true; +} + +bool NCCHContainer::DecryptToFile(std::shared_ptr dest_file, + const Common::ProgressCallback& callback) { + if (!Load()) { + return false; + } + + if (!*dest_file) { + LOG_ERROR(Core, "File is not open"); + return false; + } + + if (!is_encrypted) { + // Simply copy everything. FileDecryptor is used for progress reporting + file->Seek(0, SEEK_SET); + + const auto size = file->GetSize(); + + decryptor.SetCrypto(nullptr); + return decryptor.CryptAndWriteFile(file, size, dest_file, callback); + } + + const auto total_size = file->GetSize(); + std::size_t written{}; + + // Write NCCH header + NCCH_Header modified_header = ncch_header; + + // Set flags (equivalent to GodMode9 behaviour) + modified_header.secondary_key_slot = 0; + modified_header.fixed_key.Assign(0); + modified_header.no_crypto.Assign(1); + modified_header.seed_crypto.Assign(0); + + if (dest_file->WriteBytes(&modified_header, sizeof(modified_header)) != + sizeof(modified_header)) { + LOG_ERROR(Core, "Could not write NCCH header to file"); + return false; + } + written += sizeof(NCCH_Header); + + // Write Exheader + if (has_exheader) { + if (dest_file->WriteBytes(&exheader_header, sizeof(exheader_header)) != + sizeof(exheader_header)) { + LOG_ERROR(Core, "Could not write Exheader to file"); + return false; + } + written += sizeof(ExHeader_Header); + } + + Common::ProgressCallbackWrapper wrapper{total_size}; + const auto Write = [&](std::string_view name, std::size_t offset, std::size_t size, + bool decrypt = false, const Key::AESKey& key = {}, + const Key::AESKey& ctr = {}, std::size_t aes_seek_pos = 0) { + if (offset == 0 || size == 0) { + return true; + } + + if (aborted.exchange(false)) { + return false; + } + ASSERT_MSG(written <= offset, "Offsets are not in increasing order"); + + // Zero out the gap manually to ensure correct hashes when used with CIAs, etc. + const std::array zeroes{}; + std::size_t zeroes_left = offset - written; + while (zeroes_left > 0) { + const auto to_write = std::min(zeroes.size(), zeroes_left); + if (dest_file->WriteBytes(zeroes.data(), to_write) != to_write) { + LOG_ERROR(Core, "Could not write zeroes before {}", name); + return false; + } + zeroes_left -= to_write; + } + + file->Seek(offset, SEEK_SET); + + if (aborted.exchange(false)) { + return false; + } + + written = offset; + wrapper.SetCurrent(written); + + decryptor.SetCrypto(decrypt ? CreateCTRCrypto(key, ctr, aes_seek_pos) : nullptr); + if (!decryptor.CryptAndWriteFile(file, size, dest_file, wrapper.Wrap(callback))) { + LOG_ERROR(Core, "Could not write {}", name); + return false; + } + written = offset + size; + return true; + }; + + if (!Write("logo", ncch_header.logo_region_offset * 0x200, + ncch_header.logo_region_size * 0x200)) { + return false; + } + + if (!Write("plain region", ncch_header.plain_region_offset * 0x200, + ncch_header.plain_region_size * 0x200)) { + return false; + } + + // Write ExeFS header + if (has_exefs) { + if (dest_file->WriteBytes(&exefs_header, sizeof(exefs_header)) != sizeof(exefs_header)) { + LOG_ERROR(Core, "Could not write ExeFS header to file"); + return false; + } + written += sizeof(ExeFs_Header); + + for (unsigned section_number = 0; section_number < kMaxSections; section_number++) { + const auto& section = exefs_header.section[section_number]; + if (section.offset == 0 && section.size == 0) { // not used + continue; + } + + Key::AESKey key; + if (strcmp(section.name, "icon") == 0 || strcmp(section.name, "banner") == 0) { + key = primary_key; + } else { + key = secondary_key; + } + + // Plus 1 for the ExeFS header + if (!Write(section.name, section.offset + (ncch_header.exefs_offset + 1) * 0x200, + section.size, true, key, exefs_ctr, section.offset + sizeof(exefs_header))) { + return false; + } + } + } + + if (has_romfs && !Write("romfs", ncch_header.romfs_offset * 0x200, + ncch_header.romfs_size * 0x200, true, secondary_key, romfs_ctr)) { + return false; + } + if (written < total_size) { + LOG_WARNING(Core, "Data after {} ignored", written); + } + + callback(total_size, total_size); + return true; +} + +void NCCHContainer::AbortDecryptToFile() { + aborted = true; + decryptor.Abort(); +} + +#pragma pack(push, 1) +struct RomFSIVFCHeader { + u32_le magic; + u32_le version; + u32_le master_hash_size; + std::array levels; + INSERT_PADDING_BYTES(0xC); +}; +static_assert(sizeof(RomFSIVFCHeader) == 0x60, "Size of RomFSIVFCHeader is incorrect"); +#pragma pack(pop) + +std::vector LoadSharedRomFS(const std::vector& data) { + NCCH_Header header; + ASSERT_MSG(data.size() >= sizeof(header), "NCCH size is too small"); + std::memcpy(&header, data.data(), sizeof(header)); + + const std::size_t offset = header.romfs_offset * 0x200; // 0x200: Media unit + RomFSIVFCHeader ivfc; + ASSERT_MSG(data.size() >= offset + sizeof(ivfc), "NCCH size is too small"); + std::memcpy(&ivfc, data.data() + offset, sizeof(ivfc)); + + ASSERT_MSG(ivfc.magic == MakeMagic('I', 'V', 'F', 'C'), "IVFC magic is incorrect"); + ASSERT_MSG(ivfc.version == 0x10000, "IVFC version is incorrect"); + + std::vector result(ivfc.levels[2].size); + + // Calculation from ctrtool + const std::size_t data_offset = + offset + Common::AlignUp(sizeof(ivfc) + ivfc.master_hash_size, + std::pow(2, ivfc.levels[2].block_size)); + ASSERT_MSG(data.size() >= data_offset + ivfc.levels[2].size); + std::memcpy(result.data(), data.data() + data_offset, ivfc.levels[2].size); + + return result; +} + +} // namespace Core diff --git a/src/core/file_sys/ncch_container.h b/src/core/file_sys/ncch_container.h index d88aca1..03e3ca4 100644 --- a/src/core/file_sys/ncch_container.h +++ b/src/core/file_sys/ncch_container.h @@ -1,330 +1,330 @@ -// Copyright 2017 Citra Emulator Project / 2019 threeSD Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include -#include -#include -#include -#include "common/bit_field.h" -#include "common/common_types.h" -#include "common/file_util.h" -#include "common/progress_callback.h" -#include "common/swap.h" -#include "core/sdmc_decryptor.h" - -namespace Core { - -//////////////////////////////////////////////////////////////////////////////////////////////////// -/// NCCH (Nintendo Content Container Header) header - -struct NCCH_Header { - u8 signature[0x100]; - u32_le magic; - u32_le content_size; - u8 partition_id[8]; - u16_le maker_code; - u16_le version; - u8 reserved_0[4]; - u64_le program_id; - u8 reserved_1[0x10]; - u8 logo_region_hash[0x20]; - u8 product_code[0x10]; - u8 extended_header_hash[0x20]; - u32_le extended_header_size; - u8 reserved_2[4]; - u8 reserved_flag[3]; - u8 secondary_key_slot; - u8 platform; - enum class ContentType : u8 { - Application = 0, - SystemUpdate = 1, - Manual = 2, - Child = 3, - Trial = 4, - }; - union { - BitField<0, 1, u8> is_data; - BitField<1, 1, u8> is_executable; - BitField<2, 3, ContentType> content_type; - }; - u8 content_unit_size; - union { - BitField<0, 1, u8> fixed_key; - BitField<1, 1, u8> no_romfs; - BitField<2, 1, u8> no_crypto; - BitField<5, 1, u8> seed_crypto; - u8 raw_crypto_flags; - }; - u32_le plain_region_offset; - u32_le plain_region_size; - u32_le logo_region_offset; - u32_le logo_region_size; - u32_le exefs_offset; - u32_le exefs_size; - u32_le exefs_hash_region_size; - u8 reserved_3[4]; - u32_le romfs_offset; - u32_le romfs_size; - u32_le romfs_hash_region_size; - u8 reserved_4[4]; - u8 exefs_super_block_hash[0x20]; - u8 romfs_super_block_hash[0x20]; -}; - -static_assert(sizeof(NCCH_Header) == 0x200, "NCCH header structure size is wrong"); - -//////////////////////////////////////////////////////////////////////////////////////////////////// -// ExeFS (executable file system) headers - -struct ExeFs_SectionHeader { - char name[8]; - u32 offset; - u32 size; -}; - -struct ExeFs_Header { - ExeFs_SectionHeader section[8]; - u8 reserved[0x80]; - u8 hashes[8][0x20]; -}; - -//////////////////////////////////////////////////////////////////////////////////////////////////// -// ExHeader (executable file system header) headers - -struct ExHeader_SystemInfoFlags { - u8 reserved[5]; - u8 flag; - u8 remaster_version[2]; -}; - -struct ExHeader_CodeSegmentInfo { - u32 address; - u32 num_max_pages; - u32 code_size; -}; - -struct ExHeader_CodeSetInfo { - u8 name[8]; - ExHeader_SystemInfoFlags flags; - ExHeader_CodeSegmentInfo text; - u32 stack_size; - ExHeader_CodeSegmentInfo ro; - u8 reserved[4]; - ExHeader_CodeSegmentInfo data; - u32 bss_size; -}; - -struct ExHeader_DependencyList { - u8 program_id[0x30][8]; -}; - -struct ExHeader_SystemInfo { - u64 save_data_size; - u64_le jump_id; - u8 reserved_2[0x30]; -}; - -struct ExHeader_StorageInfo { - union { - u64_le ext_save_data_id; - // When using extended savedata access - // Prefer the ID specified in the most significant bits - BitField<40, 20, u64> extdata_id3; - BitField<20, 20, u64> extdata_id4; - BitField<0, 20, u64> extdata_id5; - }; - u8 system_save_data_id[8]; - union { - u64_le storage_accessible_unique_ids; - // When using extended savedata access - // Prefer the ID specified in the most significant bits - BitField<40, 20, u64> extdata_id0; - BitField<20, 20, u64> extdata_id1; - BitField<0, 20, u64> extdata_id2; - }; - u8 access_info[7]; - u8 other_attributes; -}; - -struct ExHeader_ARM11_SystemLocalCaps { - u64_le program_id; - u32_le core_version; - u8 reserved_flags[2]; - union { - u8 flags0; - BitField<0, 2, u8> ideal_processor; - BitField<2, 2, u8> affinity_mask; - BitField<4, 4, u8> system_mode; - }; - u8 priority; - u8 resource_limit_descriptor[0x10][2]; - ExHeader_StorageInfo storage_info; - u8 service_access_control[0x20][8]; - u8 ex_service_access_control[0x2][8]; - u8 reserved[0xf]; - u8 resource_limit_category; -}; - -struct ExHeader_ARM11_KernelCaps { - u32_le descriptors[28]; - u8 reserved[0x10]; -}; - -struct ExHeader_ARM9_AccessControl { - u8 descriptors[15]; - u8 descversion; -}; - -struct ExHeader_Header { - ExHeader_CodeSetInfo codeset_info; - ExHeader_DependencyList dependency_list; - ExHeader_SystemInfo system_info; - ExHeader_ARM11_SystemLocalCaps arm11_system_local_caps; - ExHeader_ARM11_KernelCaps arm11_kernel_caps; - ExHeader_ARM9_AccessControl arm9_access_control; - struct { - u8 signature[0x100]; - u8 ncch_public_key_modulus[0x100]; - ExHeader_ARM11_SystemLocalCaps arm11_system_local_caps; - ExHeader_ARM11_KernelCaps arm11_kernel_caps; - ExHeader_ARM9_AccessControl arm9_access_control; - } access_desc; -}; - -static_assert(sizeof(ExHeader_Header) == 0x800, "ExHeader structure size is wrong"); - -/** - * Encryption type of an importable content. - */ -enum class EncryptionType { - None, - FixedKey, - NCCHSecure1, - NCCHSecure2, - NCCHSecure3, - NCCHSecure4, -}; - -/** - * Helper which implements an interface to deal with NCCH containers which can - * contain ExeFS archives or RomFS archives for games or other applications. - * - * Note that this is heavily stripped down and can only read (primary-key - * encrypted non-code sections of) ExeFS and ExHeader by design. - */ -class NCCHContainer { -public: - NCCHContainer(std::shared_ptr file); - NCCHContainer() {} - - bool OpenFile(std::shared_ptr file); - - /** - * Ensure ExeFS and exheader is loaded and ready for reading sections - */ - bool Load(); - - /** - * Reads an application ExeFS section of an NCCH file (non-compressed, primary key only) - * @param name Name of section to read out of NCCH file - * @param buffer Vector to read data into - */ - bool LoadSectionExeFS(const char* name, std::vector& buffer); - - /** - * Get the Program ID of the NCCH container - */ - bool ReadProgramId(u64_le& program_id); - - /** - * Get the Extdata ID of the NCCH container - */ - bool ReadExtdataId(u64& extdata_id); - - /** - * Checks whether the NCCH container contains an ExeFS - * @return bool check result - */ - bool HasExeFS(); - - /** - * Checks whether the NCCH container contains an ExHeader - * @return bool check result - */ - bool HasExHeader(); - - /** - * Reads the name of the codeset. - */ - bool ReadCodesetName(std::string& name); - - /** - * Reads the product code. - */ - bool ReadProductCode(std::string& name); - - /** - * Gets encryption type (which key is used). - */ - bool ReadEncryptionType(EncryptionType& encryption); - - /** - * Gets whether seed crypto is used. - */ - bool ReadSeedCrypto(bool& used); - - /** - * Decrypts this NCCH and write to the destination file. - */ - bool DecryptToFile( - std::shared_ptr dest_file, - const Common::ProgressCallback& callback = [](u64, u64) {}); - - /** - * Aborts DecryptToFile. Simply aborts the decryptor. - */ - void AbortDecryptToFile(); - - NCCH_Header ncch_header; - ExHeader_Header exheader_header; - ExeFs_Header exefs_header; - -private: - bool has_exheader = false; - bool has_exefs = false; - bool has_romfs = false; - - bool is_loaded = false; - - bool is_encrypted = false; - // for decrypting exheader, exefs header and icon/banner section - std::array primary_key{}; - std::array secondary_key{}; // for decrypting romfs and .code section - std::array exheader_ctr{}; - std::array exefs_ctr{}; - std::array romfs_ctr{}; - - u32 exefs_offset = 0; - - std::string root_folder; - std::string filepath; - std::shared_ptr file; - std::shared_ptr exefs_file; - - // Used for DecryptToFile - FileDecryptor decryptor; - std::atomic_bool aborted{false}; - - friend class CIABuilder; -}; - -/** - * Extracts the shared RomFS from a NCCH image. - * Used for handling system archives. - */ -std::vector LoadSharedRomFS(const std::vector& data); - -} // namespace Core +// Copyright 2017 Citra Emulator Project / 2019 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include "common/bit_field.h" +#include "common/common_types.h" +#include "common/file_util.h" +#include "common/progress_callback.h" +#include "common/swap.h" +#include "core/sdmc_decryptor.h" + +namespace Core { + +//////////////////////////////////////////////////////////////////////////////////////////////////// +/// NCCH (Nintendo Content Container Header) header + +struct NCCH_Header { + u8 signature[0x100]; + u32_le magic; + u32_le content_size; + u8 partition_id[8]; + u16_le maker_code; + u16_le version; + u8 reserved_0[4]; + u64_le program_id; + u8 reserved_1[0x10]; + u8 logo_region_hash[0x20]; + u8 product_code[0x10]; + u8 extended_header_hash[0x20]; + u32_le extended_header_size; + u8 reserved_2[4]; + u8 reserved_flag[3]; + u8 secondary_key_slot; + u8 platform; + enum class ContentType : u8 { + Application = 0, + SystemUpdate = 1, + Manual = 2, + Child = 3, + Trial = 4, + }; + union { + BitField<0, 1, u8> is_data; + BitField<1, 1, u8> is_executable; + BitField<2, 3, ContentType> content_type; + }; + u8 content_unit_size; + union { + BitField<0, 1, u8> fixed_key; + BitField<1, 1, u8> no_romfs; + BitField<2, 1, u8> no_crypto; + BitField<5, 1, u8> seed_crypto; + u8 raw_crypto_flags; + }; + u32_le plain_region_offset; + u32_le plain_region_size; + u32_le logo_region_offset; + u32_le logo_region_size; + u32_le exefs_offset; + u32_le exefs_size; + u32_le exefs_hash_region_size; + u8 reserved_3[4]; + u32_le romfs_offset; + u32_le romfs_size; + u32_le romfs_hash_region_size; + u8 reserved_4[4]; + u8 exefs_super_block_hash[0x20]; + u8 romfs_super_block_hash[0x20]; +}; + +static_assert(sizeof(NCCH_Header) == 0x200, "NCCH header structure size is wrong"); + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// ExeFS (executable file system) headers + +struct ExeFs_SectionHeader { + char name[8]; + u32 offset; + u32 size; +}; + +struct ExeFs_Header { + ExeFs_SectionHeader section[8]; + u8 reserved[0x80]; + u8 hashes[8][0x20]; +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// ExHeader (executable file system header) headers + +struct ExHeader_SystemInfoFlags { + u8 reserved[5]; + u8 flag; + u8 remaster_version[2]; +}; + +struct ExHeader_CodeSegmentInfo { + u32 address; + u32 num_max_pages; + u32 code_size; +}; + +struct ExHeader_CodeSetInfo { + u8 name[8]; + ExHeader_SystemInfoFlags flags; + ExHeader_CodeSegmentInfo text; + u32 stack_size; + ExHeader_CodeSegmentInfo ro; + u8 reserved[4]; + ExHeader_CodeSegmentInfo data; + u32 bss_size; +}; + +struct ExHeader_DependencyList { + u8 program_id[0x30][8]; +}; + +struct ExHeader_SystemInfo { + u64 save_data_size; + u64_le jump_id; + u8 reserved_2[0x30]; +}; + +struct ExHeader_StorageInfo { + union { + u64_le ext_save_data_id; + // When using extended savedata access + // Prefer the ID specified in the most significant bits + BitField<40, 20, u64> extdata_id3; + BitField<20, 20, u64> extdata_id4; + BitField<0, 20, u64> extdata_id5; + }; + u8 system_save_data_id[8]; + union { + u64_le storage_accessible_unique_ids; + // When using extended savedata access + // Prefer the ID specified in the most significant bits + BitField<40, 20, u64> extdata_id0; + BitField<20, 20, u64> extdata_id1; + BitField<0, 20, u64> extdata_id2; + }; + u8 access_info[7]; + u8 other_attributes; +}; + +struct ExHeader_ARM11_SystemLocalCaps { + u64_le program_id; + u32_le core_version; + u8 reserved_flags[2]; + union { + u8 flags0; + BitField<0, 2, u8> ideal_processor; + BitField<2, 2, u8> affinity_mask; + BitField<4, 4, u8> system_mode; + }; + u8 priority; + u8 resource_limit_descriptor[0x10][2]; + ExHeader_StorageInfo storage_info; + u8 service_access_control[0x20][8]; + u8 ex_service_access_control[0x2][8]; + u8 reserved[0xf]; + u8 resource_limit_category; +}; + +struct ExHeader_ARM11_KernelCaps { + u32_le descriptors[28]; + u8 reserved[0x10]; +}; + +struct ExHeader_ARM9_AccessControl { + u8 descriptors[15]; + u8 descversion; +}; + +struct ExHeader_Header { + ExHeader_CodeSetInfo codeset_info; + ExHeader_DependencyList dependency_list; + ExHeader_SystemInfo system_info; + ExHeader_ARM11_SystemLocalCaps arm11_system_local_caps; + ExHeader_ARM11_KernelCaps arm11_kernel_caps; + ExHeader_ARM9_AccessControl arm9_access_control; + struct { + u8 signature[0x100]; + u8 ncch_public_key_modulus[0x100]; + ExHeader_ARM11_SystemLocalCaps arm11_system_local_caps; + ExHeader_ARM11_KernelCaps arm11_kernel_caps; + ExHeader_ARM9_AccessControl arm9_access_control; + } access_desc; +}; + +static_assert(sizeof(ExHeader_Header) == 0x800, "ExHeader structure size is wrong"); + +/** + * Encryption type of an importable content. + */ +enum class EncryptionType { + None, + FixedKey, + NCCHSecure1, + NCCHSecure2, + NCCHSecure3, + NCCHSecure4, +}; + +/** + * Helper which implements an interface to deal with NCCH containers which can + * contain ExeFS archives or RomFS archives for games or other applications. + * + * Note that this is heavily stripped down and can only read (primary-key + * encrypted non-code sections of) ExeFS and ExHeader by design. + */ +class NCCHContainer { +public: + NCCHContainer(std::shared_ptr file); + NCCHContainer() {} + + bool OpenFile(std::shared_ptr file); + + /** + * Ensure ExeFS and exheader is loaded and ready for reading sections + */ + bool Load(); + + /** + * Reads an application ExeFS section of an NCCH file (non-compressed, primary key only) + * @param name Name of section to read out of NCCH file + * @param buffer Vector to read data into + */ + bool LoadSectionExeFS(const char* name, std::vector& buffer); + + /** + * Get the Program ID of the NCCH container + */ + bool ReadProgramId(u64_le& program_id); + + /** + * Get the Extdata ID of the NCCH container + */ + bool ReadExtdataId(u64& extdata_id); + + /** + * Checks whether the NCCH container contains an ExeFS + * @return bool check result + */ + bool HasExeFS(); + + /** + * Checks whether the NCCH container contains an ExHeader + * @return bool check result + */ + bool HasExHeader(); + + /** + * Reads the name of the codeset. + */ + bool ReadCodesetName(std::string& name); + + /** + * Reads the product code. + */ + bool ReadProductCode(std::string& name); + + /** + * Gets encryption type (which key is used). + */ + bool ReadEncryptionType(EncryptionType& encryption); + + /** + * Gets whether seed crypto is used. + */ + bool ReadSeedCrypto(bool& used); + + /** + * Decrypts this NCCH and write to the destination file. + */ + bool DecryptToFile( + std::shared_ptr dest_file, + const Common::ProgressCallback& callback = [](u64, u64) {}); + + /** + * Aborts DecryptToFile. Simply aborts the decryptor. + */ + void AbortDecryptToFile(); + + NCCH_Header ncch_header; + ExHeader_Header exheader_header; + ExeFs_Header exefs_header; + +private: + bool has_exheader = false; + bool has_exefs = false; + bool has_romfs = false; + + bool is_loaded = false; + + bool is_encrypted = false; + // for decrypting exheader, exefs header and icon/banner section + std::array primary_key{}; + std::array secondary_key{}; // for decrypting romfs and .code section + std::array exheader_ctr{}; + std::array exefs_ctr{}; + std::array romfs_ctr{}; + + u32 exefs_offset = 0; + + std::string root_folder; + std::string filepath; + std::shared_ptr file; + std::shared_ptr exefs_file; + + // Used for DecryptToFile + FileDecryptor decryptor; + std::atomic_bool aborted{false}; + + friend class CIABuilder; +}; + +/** + * Extracts the shared RomFS from a NCCH image. + * Used for handling system archives. + */ +std::vector LoadSharedRomFS(const std::vector& data); + +} // namespace Core diff --git a/src/core/importer.h b/src/core/importer.h index 64d7109..7dcc1fc 100644 --- a/src/core/importer.h +++ b/src/core/importer.h @@ -1,244 +1,244 @@ -// Copyright 2019 threeSD Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include -#include -#include -#include -#include "common/common_types.h" -#include "common/progress_callback.h" -#include "core/file_decryptor.h" -#include "core/file_sys/cia_common.h" - -namespace Core { - -class CIABuilder; -class SDMCDecryptor; -class TicketDB; -class TitleDB; -class TitleMetadata; - -/** - * Type of an importable content. - * Applications, updates and DLCs are all considered titles. - */ -enum class ContentType { - Application, - Update, - DLC, - Savegame, - Extdata, - SystemArchive, - Sysdata, - SystemTitle, - SystemApplet, // This should belong to System Title, but they cause problems so a new category. -}; - -constexpr bool IsTitle(ContentType type) { - return type == ContentType::Application || type == ContentType::Update || - type == ContentType::DLC || type == ContentType::SystemTitle || - type == ContentType::SystemApplet; -} -constexpr bool IsNandTitle(ContentType type) { - return type == ContentType::SystemTitle || type == ContentType::SystemApplet; -} - -/** - * Struct that specifies an importable content. - */ -struct ContentSpecifier { - ContentType type; - u64 id; - bool already_exists; ///< Tells whether a file already exists in target path. - u64 maximum_size; ///< The maximum size of the content. May be slightly bigger than real size. - std::string name; ///< Optional. The content's preferred display name. - u64 extdata_id; ///< Extdata ID for Applications. - std::vector icon; ///< Optional. The content's icon. -}; - -/** - * A set of values that are used to initialize the importer. - * All paths to directories shall end with a '/' (will be automatically added when not present) - */ -struct Config { - std::string sdmc_path; ///< SDMC root path ("Nintendo 3DS//") - std::string user_path; ///< Target user path of Citra - - // Necessary system files keys are loaded from. - std::string movable_sed_path; ///< Path to movable.sed - std::string bootrom_path; ///< Path to bootrom (boot9.bin) (Sysdata 0) - std::string certs_db_path; ///< Path to certs.db. Used while building CIA. - - // Optional, used while building CIA, but usually missing these files won't hinder CIA building. - std::string nand_title_db_path; ///< Path to NAND title.db. Entirely optional. - std::string ticket_db_path; ///< Path to ticket.db. Entirely optional. - std::string enc_title_keys_bin_path; ///< Path to encTitleKeys.bin. Entireley optional. - - // The following system files are optional for importing and are only copied so that Citra - // will be able to decrypt imported encrypted ROMs. - - std::string safe_mode_firm_path; ///< Path to safe mode firm (A folder) (Sysdata 1) - std::string seed_db_path; ///< Path to seeddb.bin (Sysdata 2) - std::string secret_sector_path; ///< Path to secret sector (New3DS only) (Sysdata 3) - // Note: Sysdata 4 is aes_keys.txt (slot0x25KeyX) - std::string config_savegame_path; ///< Path to config savegame (Sysdata 5) - - std::string system_archives_path; ///< Path to system archives. - std::string system_titles_path; ///< Path to system titles. - std::string nand_data_path; ///< Path to NAND data. (Extdata and savedata) - - int version = 0; ///< Version of the dumper used. -}; - -// Version of the current dumper. -constexpr int CurrentDumperVersion = 4; - -class SDMCFile; -class NCCHContainer; - -class SDMCImporter { -public: - /** - * Initializes the importer. - * @param root_folder Path to the "Nintendo 3DS//" folder. - */ - explicit SDMCImporter(const Config& config); - - ~SDMCImporter(); - - /** - * Imports a specific content by its specifier, deleting it when failed. - * Blocks, but can be aborted on another thread if needed. - * @return true on success, false otherwise - */ - bool ImportContent( - const ContentSpecifier& specifier, - const Common::ProgressCallback& callback = [](u64, u64) {}); - - /** - * Aborts current importing. - */ - void AbortImporting(); - - /** - * Dumps a content to CXI. - * Blocks, but can be aborted on another thread. - * @return true on success, false otherwise - */ - bool DumpCXI(const ContentSpecifier& specifier, std::string destination, - const Common::ProgressCallback& callback, bool auto_filename = false); - - /** - * Aborts current CXI dumping. - */ - void AbortDumpCXI(); - - /** - * Builds a CIA from a content. - * Blocks, but can be aborted on another thread. - * @return true on success, false otherwise - */ - bool BuildCIA(CIABuildType build_type, const ContentSpecifier& specifier, - std::string destination, const Common::ProgressCallback& callback, - bool auto_filename = false); - - /** - * Checks if a content can be built as a legit CIA. - */ - bool CanBuildLegitCIA(const ContentSpecifier& specifier) const; - - /** - * Aborts current CIA building - */ - void AbortBuildCIA(); - - /** - * Checks the contents of a title against its TMD hashes. - */ - bool CheckTitleContents( - const ContentSpecifier& specifier, - const Common::ProgressCallback& callback = [](u64, u64) {}); - - /** - * Gets a list of dumpable content specifiers. - */ - std::vector ListContent() const; - - /** - * Returns whether the importer is in good state. - */ - bool IsGood() const; - - bool LoadTMD(ContentType type, u64 id, TitleMetadata& out) const; - bool LoadTMD(const ContentSpecifier& specifier, TitleMetadata& out) const; - - std::string GetTitleContentsPath(const ContentSpecifier& specifier) const; - std::shared_ptr OpenContent(const ContentSpecifier& specifier, - u32 content_id) const; - - std::shared_ptr& GetTicketDB() { - return ticket_db; - } - - const std::shared_ptr& GetTicketDB() const { - return ticket_db; - } - -private: - bool Init(); - - // Impl of ImportContent without deleting mechanism. - bool ImportContentImpl( - const ContentSpecifier& specifier, - const Common::ProgressCallback& callback = [](u64, u64) {}); - bool ImportTitle(const ContentSpecifier& specifier, const Common::ProgressCallback& callback); - bool ImportNandTitle(const ContentSpecifier& specifier, - const Common::ProgressCallback& callback); - bool ImportSavegame(u64 id, const Common::ProgressCallback& callback); - bool ImportNandSavegame(u64 id, const Common::ProgressCallback& callback); - bool ImportExtdata(u64 id, const Common::ProgressCallback& callback); - bool ImportNandExtdata(u64 id, const Common::ProgressCallback& callback); - bool ImportSystemArchive(u64 id, const Common::ProgressCallback& callback); - bool ImportSysdata(u64 id, const Common::ProgressCallback& callback); - - void ListTitle(std::vector& out) const; - void ListNandTitle(std::vector& out) const; - void ListNandSavegame(std::vector& out) const; - void ListExtdata(std::vector& out) const; - void ListSystemArchive(std::vector& out) const; - void ListSysdata(std::vector& out) const; - - void DeleteContent(const ContentSpecifier& specifier) const; - void DeleteTitle(u64 id) const; - void DeleteNandTitle(u64 id) const; - void DeleteSavegame(u64 id) const; - void DeleteExtdata(u64 id) const; - void DeleteSystemArchive(u64 id) const; - void DeleteSysdata(u64 id) const; - - bool is_good{}; - Config config; - std::unique_ptr sdmc_decryptor; - FileDecryptor file_decryptor; - - // Used for CIA building - std::unique_ptr cia_builder; - std::shared_ptr ticket_db; - - // The NCCH used to dump CXIs. - std::unique_ptr dump_cxi_ncch; - - std::unique_ptr sdmc_title_db{}; - std::unique_ptr nand_title_db{}; -}; - -/** - * Look for and load preset config for a SD card mounted at mount_point. - * @return a list of preset config available. can be empty - */ -std::vector LoadPresetConfig(std::string mount_point); - -} // namespace Core +// Copyright 2019 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include "common/common_types.h" +#include "common/progress_callback.h" +#include "core/file_decryptor.h" +#include "core/file_sys/cia_common.h" + +namespace Core { + +class CIABuilder; +class SDMCDecryptor; +class TicketDB; +class TitleDB; +class TitleMetadata; + +/** + * Type of an importable content. + * Applications, updates and DLCs are all considered titles. + */ +enum class ContentType { + Application, + Update, + DLC, + Savegame, + Extdata, + SystemArchive, + Sysdata, + SystemTitle, + SystemApplet, // This should belong to System Title, but they cause problems so a new category. +}; + +constexpr bool IsTitle(ContentType type) { + return type == ContentType::Application || type == ContentType::Update || + type == ContentType::DLC || type == ContentType::SystemTitle || + type == ContentType::SystemApplet; +} +constexpr bool IsNandTitle(ContentType type) { + return type == ContentType::SystemTitle || type == ContentType::SystemApplet; +} + +/** + * Struct that specifies an importable content. + */ +struct ContentSpecifier { + ContentType type; + u64 id; + bool already_exists; ///< Tells whether a file already exists in target path. + u64 maximum_size; ///< The maximum size of the content. May be slightly bigger than real size. + std::string name; ///< Optional. The content's preferred display name. + u64 extdata_id; ///< Extdata ID for Applications. + std::vector icon; ///< Optional. The content's icon. +}; + +/** + * A set of values that are used to initialize the importer. + * All paths to directories shall end with a '/' (will be automatically added when not present) + */ +struct Config { + std::string sdmc_path; ///< SDMC root path ("Nintendo 3DS//") + std::string user_path; ///< Target user path of Citra + + // Necessary system files keys are loaded from. + std::string movable_sed_path; ///< Path to movable.sed + std::string bootrom_path; ///< Path to bootrom (boot9.bin) (Sysdata 0) + std::string certs_db_path; ///< Path to certs.db. Used while building CIA. + + // Optional, used while building CIA, but usually missing these files won't hinder CIA building. + std::string nand_title_db_path; ///< Path to NAND title.db. Entirely optional. + std::string ticket_db_path; ///< Path to ticket.db. Entirely optional. + std::string enc_title_keys_bin_path; ///< Path to encTitleKeys.bin. Entireley optional. + + // The following system files are optional for importing and are only copied so that Citra + // will be able to decrypt imported encrypted ROMs. + + std::string safe_mode_firm_path; ///< Path to safe mode firm (A folder) (Sysdata 1) + std::string seed_db_path; ///< Path to seeddb.bin (Sysdata 2) + std::string secret_sector_path; ///< Path to secret sector (New3DS only) (Sysdata 3) + // Note: Sysdata 4 is aes_keys.txt (slot0x25KeyX) + std::string config_savegame_path; ///< Path to config savegame (Sysdata 5) + + std::string system_archives_path; ///< Path to system archives. + std::string system_titles_path; ///< Path to system titles. + std::string nand_data_path; ///< Path to NAND data. (Extdata and savedata) + + int version = 0; ///< Version of the dumper used. +}; + +// Version of the current dumper. +constexpr int CurrentDumperVersion = 4; + +class SDMCFile; +class NCCHContainer; + +class SDMCImporter { +public: + /** + * Initializes the importer. + * @param root_folder Path to the "Nintendo 3DS//" folder. + */ + explicit SDMCImporter(const Config& config); + + ~SDMCImporter(); + + /** + * Imports a specific content by its specifier, deleting it when failed. + * Blocks, but can be aborted on another thread if needed. + * @return true on success, false otherwise + */ + bool ImportContent( + const ContentSpecifier& specifier, + const Common::ProgressCallback& callback = [](u64, u64) {}); + + /** + * Aborts current importing. + */ + void AbortImporting(); + + /** + * Dumps a content to CXI. + * Blocks, but can be aborted on another thread. + * @return true on success, false otherwise + */ + bool DumpCXI(const ContentSpecifier& specifier, std::string destination, + const Common::ProgressCallback& callback, bool auto_filename = false); + + /** + * Aborts current CXI dumping. + */ + void AbortDumpCXI(); + + /** + * Builds a CIA from a content. + * Blocks, but can be aborted on another thread. + * @return true on success, false otherwise + */ + bool BuildCIA(CIABuildType build_type, const ContentSpecifier& specifier, + std::string destination, const Common::ProgressCallback& callback, + bool auto_filename = false); + + /** + * Checks if a content can be built as a legit CIA. + */ + bool CanBuildLegitCIA(const ContentSpecifier& specifier) const; + + /** + * Aborts current CIA building + */ + void AbortBuildCIA(); + + /** + * Checks the contents of a title against its TMD hashes. + */ + bool CheckTitleContents( + const ContentSpecifier& specifier, + const Common::ProgressCallback& callback = [](u64, u64) {}); + + /** + * Gets a list of dumpable content specifiers. + */ + std::vector ListContent() const; + + /** + * Returns whether the importer is in good state. + */ + bool IsGood() const; + + bool LoadTMD(ContentType type, u64 id, TitleMetadata& out) const; + bool LoadTMD(const ContentSpecifier& specifier, TitleMetadata& out) const; + + std::string GetTitleContentsPath(const ContentSpecifier& specifier) const; + std::shared_ptr OpenContent(const ContentSpecifier& specifier, + u32 content_id) const; + + std::shared_ptr& GetTicketDB() { + return ticket_db; + } + + const std::shared_ptr& GetTicketDB() const { + return ticket_db; + } + +private: + bool Init(); + + // Impl of ImportContent without deleting mechanism. + bool ImportContentImpl( + const ContentSpecifier& specifier, + const Common::ProgressCallback& callback = [](u64, u64) {}); + bool ImportTitle(const ContentSpecifier& specifier, const Common::ProgressCallback& callback); + bool ImportNandTitle(const ContentSpecifier& specifier, + const Common::ProgressCallback& callback); + bool ImportSavegame(u64 id, const Common::ProgressCallback& callback); + bool ImportNandSavegame(u64 id, const Common::ProgressCallback& callback); + bool ImportExtdata(u64 id, const Common::ProgressCallback& callback); + bool ImportNandExtdata(u64 id, const Common::ProgressCallback& callback); + bool ImportSystemArchive(u64 id, const Common::ProgressCallback& callback); + bool ImportSysdata(u64 id, const Common::ProgressCallback& callback); + + void ListTitle(std::vector& out) const; + void ListNandTitle(std::vector& out) const; + void ListNandSavegame(std::vector& out) const; + void ListExtdata(std::vector& out) const; + void ListSystemArchive(std::vector& out) const; + void ListSysdata(std::vector& out) const; + + void DeleteContent(const ContentSpecifier& specifier) const; + void DeleteTitle(u64 id) const; + void DeleteNandTitle(u64 id) const; + void DeleteSavegame(u64 id) const; + void DeleteExtdata(u64 id) const; + void DeleteSystemArchive(u64 id) const; + void DeleteSysdata(u64 id) const; + + bool is_good{}; + Config config; + std::unique_ptr sdmc_decryptor; + FileDecryptor file_decryptor; + + // Used for CIA building + std::unique_ptr cia_builder; + std::shared_ptr ticket_db; + + // The NCCH used to dump CXIs. + std::unique_ptr dump_cxi_ncch; + + std::unique_ptr sdmc_title_db{}; + std::unique_ptr nand_title_db{}; +}; + +/** + * Look for and load preset config for a SD card mounted at mount_point. + * @return a list of preset config available. can be empty + */ +std::vector LoadPresetConfig(std::string mount_point); + +} // namespace Core diff --git a/src/frontend/CMakeLists.txt b/src/frontend/CMakeLists.txt index 562b07a..a2f0b10 100644 --- a/src/frontend/CMakeLists.txt +++ b/src/frontend/CMakeLists.txt @@ -1,88 +1,88 @@ -set(CMAKE_AUTOMOC ON) -set(CMAKE_AUTORCC ON) -set(CMAKE_AUTOUIC ON) -set(CMAKE_INCLUDE_CURRENT_DIR ON) -if (POLICY CMP0071) - cmake_policy(SET CMP0071 NEW) -endif() - -file(GLOB_RECURSE THEMES ${PROJECT_SOURCE_DIR}/dist/themes/*) - -add_executable(threeSD - helpers/dpi_aware_dialog.cpp - helpers/dpi_aware_dialog.h - helpers/frontend_common.cpp - helpers/frontend_common.h - helpers/multi_job.cpp - helpers/multi_job.h - helpers/rate_limited_progress_dialog.cpp - helpers/rate_limited_progress_dialog.h - helpers/simple_job.cpp - helpers/simple_job.h - cia_build_dialog.cpp - cia_build_dialog.h - cia_build_dialog.ui - import_dialog.cpp - import_dialog.h - import_dialog.ui - main.cpp - main.h - main.ui - select_files_dialog.cpp - select_files_dialog.h - select_files_dialog.ui - title_info_dialog.cpp - title_info_dialog.h - title_info_dialog.ui - utilities.cpp - utilities.h - utilities.ui - ${THEMES} -) - -target_link_libraries(threeSD PRIVATE common core) -target_link_libraries(threeSD PRIVATE Qt5::Widgets) -target_link_libraries(threeSD PRIVATE qdevicewatcher) -target_link_libraries(threeSD PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads) - -if (APPLE) - # set(MACOSX_ICON "../../dist/citra.icns") - # set_source_files_properties(${MACOSX_ICON} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) - # target_sources(threeSD PRIVATE ${MACOSX_ICON}) - set_target_properties(threeSD PROPERTIES MACOSX_BUNDLE TRUE) - set_target_properties(threeSD PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist) -elseif(WIN32) - # compile as a win32 gui application instead of a console application - target_link_libraries(threeSD PRIVATE Qt5::WinMain) - if(MSVC) - set_target_properties(threeSD PROPERTIES LINK_FLAGS_RELEASE "/SUBSYSTEM:WINDOWS") - elseif(MINGW) - set_target_properties(threeSD PROPERTIES LINK_FLAGS_RELEASE "-mwindows") - endif() -elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") - # In Ubuntu, the executable would be recognized as a shared library otherwise. - set_target_properties(threeSD PROPERTIES LINK_FLAGS "-no-pie") -endif() - -target_compile_definitions(threeSD PRIVATE - # Use QStringBuilder for string concatenation to reduce - # the overall number of temporary strings created. - -DQT_USE_QSTRINGBUILDER - - # Disable implicit type narrowing in signal/slot connect() calls. - -DQT_NO_NARROWING_CONVERSIONS_IN_CONNECT - - # Disable unsafe overloads of QProcess' start() function. - -DQT_NO_PROCESS_COMBINED_ARGUMENT_START - - # Disable implicit QString->QUrl conversions to enforce use of proper resolving functions. - -DQT_NO_URL_CAST_FROM_STRING - - # Disable automatic conversions from 8-bit strings (char *) to unicode QStrings - -DQT_NO_CAST_FROM_ASCII -) - -if (MSVC) - include(CopyQt5Deps) - copy_Qt5_deps(threeSD) -endif() +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_INCLUDE_CURRENT_DIR ON) +if (POLICY CMP0071) + cmake_policy(SET CMP0071 NEW) +endif() + +file(GLOB_RECURSE THEMES ${PROJECT_SOURCE_DIR}/dist/themes/*) + +add_executable(threeSD + helpers/dpi_aware_dialog.cpp + helpers/dpi_aware_dialog.h + helpers/frontend_common.cpp + helpers/frontend_common.h + helpers/multi_job.cpp + helpers/multi_job.h + helpers/rate_limited_progress_dialog.cpp + helpers/rate_limited_progress_dialog.h + helpers/simple_job.cpp + helpers/simple_job.h + cia_build_dialog.cpp + cia_build_dialog.h + cia_build_dialog.ui + import_dialog.cpp + import_dialog.h + import_dialog.ui + main.cpp + main.h + main.ui + select_files_dialog.cpp + select_files_dialog.h + select_files_dialog.ui + title_info_dialog.cpp + title_info_dialog.h + title_info_dialog.ui + utilities.cpp + utilities.h + utilities.ui + ${THEMES} +) + +target_link_libraries(threeSD PRIVATE common core) +target_link_libraries(threeSD PRIVATE Qt5::Widgets) +target_link_libraries(threeSD PRIVATE qdevicewatcher) +target_link_libraries(threeSD PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads) + +if (APPLE) + # set(MACOSX_ICON "../../dist/citra.icns") + # set_source_files_properties(${MACOSX_ICON} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) + # target_sources(threeSD PRIVATE ${MACOSX_ICON}) + set_target_properties(threeSD PROPERTIES MACOSX_BUNDLE TRUE) + set_target_properties(threeSD PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist) +elseif(WIN32) + # compile as a win32 gui application instead of a console application + target_link_libraries(threeSD PRIVATE Qt5::WinMain) + if(MSVC) + set_target_properties(threeSD PROPERTIES LINK_FLAGS_RELEASE "/SUBSYSTEM:WINDOWS") + elseif(MINGW) + set_target_properties(threeSD PROPERTIES LINK_FLAGS_RELEASE "-mwindows") + endif() +elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") + # In Ubuntu, the executable would be recognized as a shared library otherwise. + set_target_properties(threeSD PROPERTIES LINK_FLAGS "-no-pie") +endif() + +target_compile_definitions(threeSD PRIVATE + # Use QStringBuilder for string concatenation to reduce + # the overall number of temporary strings created. + -DQT_USE_QSTRINGBUILDER + + # Disable implicit type narrowing in signal/slot connect() calls. + -DQT_NO_NARROWING_CONVERSIONS_IN_CONNECT + + # Disable unsafe overloads of QProcess' start() function. + -DQT_NO_PROCESS_COMBINED_ARGUMENT_START + + # Disable implicit QString->QUrl conversions to enforce use of proper resolving functions. + -DQT_NO_URL_CAST_FROM_STRING + + # Disable automatic conversions from 8-bit strings (char *) to unicode QStrings + -DQT_NO_CAST_FROM_ASCII +) + +if (MSVC) + include(CopyQt5Deps) + copy_Qt5_deps(threeSD) +endif() diff --git a/src/frontend/cia_build_dialog.cpp b/src/frontend/cia_build_dialog.cpp index 2a7aba0..dede332 100644 --- a/src/frontend/cia_build_dialog.cpp +++ b/src/frontend/cia_build_dialog.cpp @@ -1,94 +1,94 @@ -// Copyright 2021 threeSD Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include -#include -#include -#include -#include "common/assert.h" -#include "frontend/cia_build_dialog.h" -#include "ui_cia_build_dialog.h" - -CIABuildDialog::CIABuildDialog(QWidget* parent, bool is_dir_, bool is_nand, bool enable_legit, - const QString& default_path) - : DPIAwareDialog(parent, 510, 260), ui(std::make_unique()), - is_dir(is_dir_) { - - ui->setupUi(this); - setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint)); - - if (is_dir) { - setWindowTitle(tr("Batch Build CIA")); - } - if (is_nand) { - ui->pirateLegitButton->setVisible(false); - ui->pirateLegitLabel->setVisible(false); - - auto message = tr("Encrypted CIA with legit TMD, encrypted contents and legit ticket.
"); - if (is_dir) { - message.append(tr( - "Legit tickets for these titles do not include console-identifying information.")); - } else { - message.append(tr( - "Legit ticket for this title does not include console-identifying information.")); - } - ui->legitLabel->setText(message); - } - if (!enable_legit) { - const auto message = - is_dir ? tr("This option is not available as some of the titles are not legit.") - : tr("This option is not available as the title is not legit."); - ui->pirateLegitButton->setEnabled(false); - ui->pirateLegitLabel->setText(message); - ui->legitButton->setEnabled(false); - ui->legitLabel->setText(message); - } - - connect(ui->buttonBox, &QDialogButtonBox::accepted, [this] { - if (ui->destination->text().isEmpty()) { - const QString message = is_dir ? tr("Please specify destination folder.") - : tr("Please specify destination file."); - QMessageBox::warning(this, tr("threeSD"), message); - return; - } - accept(); - }); - connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &CIABuildDialog::reject); - - if (is_dir) { - ui->destination->setText(default_path); - } - connect(ui->destinationExplore, &QToolButton::clicked, [this, default_path] { - QString path; - if (is_dir) { - path = QFileDialog::getExistingDirectory(this, tr("Batch Build CIA"), - ui->destination->text()); - } else { - const auto cur = ui->destination->text().isEmpty() - ? default_path - : QFileInfo(ui->destination->text()).path(); - path = QFileDialog::getSaveFileName(this, tr("Build CIA"), cur, - tr("CTR Importable Archive (*.cia)")); - } - if (!path.isEmpty()) { - ui->destination->setText(path); - } - }); -} - -CIABuildDialog::~CIABuildDialog() = default; - -std::pair CIABuildDialog::GetResults() const { - Core::CIABuildType type; - if (ui->standardButton->isChecked()) { - type = Core::CIABuildType::Standard; - } else if (ui->pirateLegitButton->isChecked()) { - type = Core::CIABuildType::PirateLegit; - } else if (ui->legitButton->isChecked()) { - type = Core::CIABuildType::Legit; - } else { - UNREACHABLE(); - } - return {ui->destination->text(), type}; -} +// Copyright 2021 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include "common/assert.h" +#include "frontend/cia_build_dialog.h" +#include "ui_cia_build_dialog.h" + +CIABuildDialog::CIABuildDialog(QWidget* parent, bool is_dir_, bool is_nand, bool enable_legit, + const QString& default_path) + : DPIAwareDialog(parent, 510, 260), ui(std::make_unique()), + is_dir(is_dir_) { + + ui->setupUi(this); + setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint)); + + if (is_dir) { + setWindowTitle(tr("Batch Build CIA")); + } + if (is_nand) { + ui->pirateLegitButton->setVisible(false); + ui->pirateLegitLabel->setVisible(false); + + auto message = tr("Encrypted CIA with legit TMD, encrypted contents and legit ticket.
"); + if (is_dir) { + message.append(tr( + "Legit tickets for these titles do not include console-identifying information.")); + } else { + message.append(tr( + "Legit ticket for this title does not include console-identifying information.")); + } + ui->legitLabel->setText(message); + } + if (!enable_legit) { + const auto message = + is_dir ? tr("This option is not available as some of the titles are not legit.") + : tr("This option is not available as the title is not legit."); + ui->pirateLegitButton->setEnabled(false); + ui->pirateLegitLabel->setText(message); + ui->legitButton->setEnabled(false); + ui->legitLabel->setText(message); + } + + connect(ui->buttonBox, &QDialogButtonBox::accepted, [this] { + if (ui->destination->text().isEmpty()) { + const QString message = is_dir ? tr("Please specify destination folder.") + : tr("Please specify destination file."); + QMessageBox::warning(this, tr("threeSD"), message); + return; + } + accept(); + }); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &CIABuildDialog::reject); + + if (is_dir) { + ui->destination->setText(default_path); + } + connect(ui->destinationExplore, &QToolButton::clicked, [this, default_path] { + QString path; + if (is_dir) { + path = QFileDialog::getExistingDirectory(this, tr("Batch Build CIA"), + ui->destination->text()); + } else { + const auto cur = ui->destination->text().isEmpty() + ? default_path + : QFileInfo(ui->destination->text()).path(); + path = QFileDialog::getSaveFileName(this, tr("Build CIA"), cur, + tr("CTR Importable Archive (*.cia)")); + } + if (!path.isEmpty()) { + ui->destination->setText(path); + } + }); +} + +CIABuildDialog::~CIABuildDialog() = default; + +std::pair CIABuildDialog::GetResults() const { + Core::CIABuildType type; + if (ui->standardButton->isChecked()) { + type = Core::CIABuildType::Standard; + } else if (ui->pirateLegitButton->isChecked()) { + type = Core::CIABuildType::PirateLegit; + } else if (ui->legitButton->isChecked()) { + type = Core::CIABuildType::Legit; + } else { + UNREACHABLE(); + } + return {ui->destination->text(), type}; +} diff --git a/src/frontend/cia_build_dialog.h b/src/frontend/cia_build_dialog.h index 03d6c7f..0c805a5 100644 --- a/src/frontend/cia_build_dialog.h +++ b/src/frontend/cia_build_dialog.h @@ -1,29 +1,29 @@ -// Copyright 2021 threeSD Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include -#include -#include "core/file_sys/cia_common.h" -#include "frontend/helpers/dpi_aware_dialog.h" - -namespace Ui { -class CIABuildDialog; -} - -class CIABuildDialog : public DPIAwareDialog { - Q_OBJECT - -public: - explicit CIABuildDialog(QWidget* parent, bool is_dir, bool is_nand, bool enable_legit, - const QString& default_path); - ~CIABuildDialog(); - - std::pair GetResults() const; - -private: - std::unique_ptr ui; - bool is_dir; -}; +// Copyright 2021 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include "core/file_sys/cia_common.h" +#include "frontend/helpers/dpi_aware_dialog.h" + +namespace Ui { +class CIABuildDialog; +} + +class CIABuildDialog : public DPIAwareDialog { + Q_OBJECT + +public: + explicit CIABuildDialog(QWidget* parent, bool is_dir, bool is_nand, bool enable_legit, + const QString& default_path); + ~CIABuildDialog(); + + std::pair GetResults() const; + +private: + std::unique_ptr ui; + bool is_dir; +}; diff --git a/src/frontend/cia_build_dialog.ui b/src/frontend/cia_build_dialog.ui index 519b87a..28aa4c7 100644 --- a/src/frontend/cia_build_dialog.ui +++ b/src/frontend/cia_build_dialog.ui @@ -1,108 +1,108 @@ - - - CIABuildDialog - - - Build CIA - - - - - - - - Destination: - - - - - - - - - - - 0 - 0 - - - - ... - - - - - - - - - Build Type - - - - - - Standard - - - true - - - - - - - Recommended for general use.<br>Decrypted CIA with decrypted contents and standard ticket. - - - - - - - Legit with standard ticket ("pirate legit") - - - - - - - Encrypted CIA with legit TMD, encrypted contents and standard ticket. - - - - - - - Legit - - - - - - - Encrypted CIA with legit TMD, encrypted contents and legit ticket.<br>WARNING: Legit ticket may include console identifying information! - - - - - - - - - - Qt::Vertical - - - - - - - QDialogButtonBox::Ok|QDialogButtonBox::Cancel - - - - - - - - + + + CIABuildDialog + + + Build CIA + + + + + + + + Destination: + + + + + + + + + + + 0 + 0 + + + + ... + + + + + + + + + Build Type + + + + + + Standard + + + true + + + + + + + Recommended for general use.<br>Decrypted CIA with decrypted contents and standard ticket. + + + + + + + Legit with standard ticket ("pirate legit") + + + + + + + Encrypted CIA with legit TMD, encrypted contents and standard ticket. + + + + + + + Legit + + + + + + + Encrypted CIA with legit TMD, encrypted contents and legit ticket.<br>WARNING: Legit ticket may include console identifying information! + + + + + + + + + + Qt::Vertical + + + + + + + QDialogButtonBox::Ok|QDialogButtonBox::Cancel + + + + + + + + diff --git a/src/frontend/helpers/dpi_aware_dialog.cpp b/src/frontend/helpers/dpi_aware_dialog.cpp index 147baa0..0cedbfc 100644 --- a/src/frontend/helpers/dpi_aware_dialog.cpp +++ b/src/frontend/helpers/dpi_aware_dialog.cpp @@ -1,69 +1,69 @@ -// Copyright 2021 threeSD Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include -#include -#include "common/logging/log.h" -#include "frontend/helpers/dpi_aware_dialog.h" - -DPIAwareDialog::DPIAwareDialog(QWidget* parent, int width, int height) - : QDialog(parent), original_width(width), original_height(height) {} - -DPIAwareDialog::~DPIAwareDialog() = default; - -void DPIAwareDialog::showEvent(QShowEvent* event) { - QDialog::showEvent(event); - if (window_handle) { - return; - } - - // Initialize window_handle and connections - window_handle = windowHandle(); - if (!window_handle) { - return; - } - -#ifdef __APPLE__ - // Note: macOS implements system level virtualization, so there's no need to connect here. - // but we still need to call SetContentSizes() at least once to set up the UI - // macOS style has more padding. Make the dialog larger for compensation. - resize(original_width * 1.25, original_height * 1.25); - SetContentSizes(); -#else - resized = false; - connect(window_handle, &QWindow::screenChanged, this, &DPIAwareDialog::OnScreenChanged); - OnScreenChanged(); -#endif -} - -#ifndef __APPLE__ -void DPIAwareDialog::resizeEvent(QResizeEvent* event) { - QDialog::resizeEvent(event); - resized = true; -} - -void DPIAwareDialog::OnScreenChanged() { - // Resize according to DPI - const double scaleX = window_handle->screen()->logicalDotsPerInchX() / 96.0; - const double scaleY = window_handle->screen()->logicalDotsPerInchY() / 96.0; - if (resized) { - const int new_width = static_cast(scaleX * width() / previous_scaleX); - const int new_height = static_cast(scaleY * height() / previous_scaleY); - setMinimumSize(0, 0); // Enforce this resize - resize(new_width, new_height); - } else { - const int new_width = static_cast(original_width * scaleX); - const int new_height = static_cast(original_height * scaleY); - setMinimumSize(new_width, new_height); - adjustSize(); - resized = false; // This resize isn't user-initiated - } - - SetContentSizes(previous_width, previous_height); - previous_scaleX = scaleX; - previous_scaleY = scaleY; - previous_width = width(); - previous_height = height(); -} -#endif +// Copyright 2021 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include "common/logging/log.h" +#include "frontend/helpers/dpi_aware_dialog.h" + +DPIAwareDialog::DPIAwareDialog(QWidget* parent, int width, int height) + : QDialog(parent), original_width(width), original_height(height) {} + +DPIAwareDialog::~DPIAwareDialog() = default; + +void DPIAwareDialog::showEvent(QShowEvent* event) { + QDialog::showEvent(event); + if (window_handle) { + return; + } + + // Initialize window_handle and connections + window_handle = windowHandle(); + if (!window_handle) { + return; + } + +#ifdef __APPLE__ + // Note: macOS implements system level virtualization, so there's no need to connect here. + // but we still need to call SetContentSizes() at least once to set up the UI + // macOS style has more padding. Make the dialog larger for compensation. + resize(original_width * 1.25, original_height * 1.25); + SetContentSizes(); +#else + resized = false; + connect(window_handle, &QWindow::screenChanged, this, &DPIAwareDialog::OnScreenChanged); + OnScreenChanged(); +#endif +} + +#ifndef __APPLE__ +void DPIAwareDialog::resizeEvent(QResizeEvent* event) { + QDialog::resizeEvent(event); + resized = true; +} + +void DPIAwareDialog::OnScreenChanged() { + // Resize according to DPI + const double scaleX = window_handle->screen()->logicalDotsPerInchX() / 96.0; + const double scaleY = window_handle->screen()->logicalDotsPerInchY() / 96.0; + if (resized) { + const int new_width = static_cast(scaleX * width() / previous_scaleX); + const int new_height = static_cast(scaleY * height() / previous_scaleY); + setMinimumSize(0, 0); // Enforce this resize + resize(new_width, new_height); + } else { + const int new_width = static_cast(original_width * scaleX); + const int new_height = static_cast(original_height * scaleY); + setMinimumSize(new_width, new_height); + adjustSize(); + resized = false; // This resize isn't user-initiated + } + + SetContentSizes(previous_width, previous_height); + previous_scaleX = scaleX; + previous_scaleY = scaleY; + previous_width = width(); + previous_height = height(); +} +#endif diff --git a/src/frontend/helpers/dpi_aware_dialog.h b/src/frontend/helpers/dpi_aware_dialog.h index 801a210..4a0ed0c 100644 --- a/src/frontend/helpers/dpi_aware_dialog.h +++ b/src/frontend/helpers/dpi_aware_dialog.h @@ -1,39 +1,39 @@ -// Copyright 2021 threeSD Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include - -class DPIAwareDialog : public QDialog { -public: - explicit DPIAwareDialog(QWidget* parent, int width, int height); - ~DPIAwareDialog() override; - -protected: - void showEvent(QShowEvent* event) override; - - // Called with two zeroes to set up content sizes that are relative to dialog size. Also called - // when screen is changed, to update those sizes. - virtual void SetContentSizes(int previous_width = 0, int previous_height = 0){}; - -private: - QWindow* window_handle{}; - const int original_width{}; - const int original_height{}; - -#ifndef __APPLE__ -protected: - void resizeEvent(QResizeEvent* event) override; - -private: - void OnScreenChanged(); - - bool resized = false; // whether this dialog has been manually resized - double previous_scaleX{}; - double previous_scaleY{}; - int previous_width{}; - int previous_height{}; -#endif -}; +// Copyright 2021 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include + +class DPIAwareDialog : public QDialog { +public: + explicit DPIAwareDialog(QWidget* parent, int width, int height); + ~DPIAwareDialog() override; + +protected: + void showEvent(QShowEvent* event) override; + + // Called with two zeroes to set up content sizes that are relative to dialog size. Also called + // when screen is changed, to update those sizes. + virtual void SetContentSizes(int previous_width = 0, int previous_height = 0){}; + +private: + QWindow* window_handle{}; + const int original_width{}; + const int original_height{}; + +#ifndef __APPLE__ +protected: + void resizeEvent(QResizeEvent* event) override; + +private: + void OnScreenChanged(); + + bool resized = false; // whether this dialog has been manually resized + double previous_scaleX{}; + double previous_scaleY{}; + int previous_width{}; + int previous_height{}; +#endif +}; diff --git a/src/frontend/helpers/frontend_common.cpp b/src/frontend/helpers/frontend_common.cpp index 222569c..4281122 100644 --- a/src/frontend/helpers/frontend_common.cpp +++ b/src/frontend/helpers/frontend_common.cpp @@ -1,21 +1,21 @@ -// Copyright 2021 threeSD Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include -#include -#include -#include "frontend/helpers/frontend_common.h" - -QString ReadableByteSize(qulonglong size) { - static const std::array units = {QT_TR_NOOP("B"), QT_TR_NOOP("KiB"), - QT_TR_NOOP("MiB"), QT_TR_NOOP("GiB"), - QT_TR_NOOP("TiB"), QT_TR_NOOP("PiB")}; - if (size == 0) - return QStringLiteral("0"); - int digit_groups = std::min(static_cast(std::log10(size) / std::log10(1024)), - static_cast(units.size() - 1)); - return QStringLiteral("%L1 %2") - .arg(size / std::pow(1024, digit_groups), 0, 'f', 1) - .arg(QObject::tr(units[digit_groups], "FrontendCommon")); -} +// Copyright 2021 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include "frontend/helpers/frontend_common.h" + +QString ReadableByteSize(qulonglong size) { + static const std::array units = {QT_TR_NOOP("B"), QT_TR_NOOP("KiB"), + QT_TR_NOOP("MiB"), QT_TR_NOOP("GiB"), + QT_TR_NOOP("TiB"), QT_TR_NOOP("PiB")}; + if (size == 0) + return QStringLiteral("0"); + int digit_groups = std::min(static_cast(std::log10(size) / std::log10(1024)), + static_cast(units.size() - 1)); + return QStringLiteral("%L1 %2") + .arg(size / std::pow(1024, digit_groups), 0, 'f', 1) + .arg(QObject::tr(units[digit_groups], "FrontendCommon")); +} diff --git a/src/frontend/helpers/frontend_common.h b/src/frontend/helpers/frontend_common.h index 8f65138..4a410e7 100644 --- a/src/frontend/helpers/frontend_common.h +++ b/src/frontend/helpers/frontend_common.h @@ -1,9 +1,9 @@ -// Copyright 2021 threeSD Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include - -QString ReadableByteSize(qulonglong size); +// Copyright 2021 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include + +QString ReadableByteSize(qulonglong size); diff --git a/src/frontend/helpers/multi_job.cpp b/src/frontend/helpers/multi_job.cpp index 09d1ac3..07088a7 100644 --- a/src/frontend/helpers/multi_job.cpp +++ b/src/frontend/helpers/multi_job.cpp @@ -1,67 +1,67 @@ -// Copyright 2019 threeSD Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include -#include "frontend/helpers/multi_job.h" - -MultiJob::MultiJob(QObject* parent, Core::SDMCImporter& importer_, - std::vector contents_, ExecuteFunc execute_func_, - AbortFunc abort_func_) - : QThread(parent), importer(importer_), contents(std::move(contents_)), - execute_func(std::move(execute_func_)), abort_func(abort_func_) {} - -MultiJob::~MultiJob() = default; - -void MultiJob::run() { - u64 total_size = 0; - for (const auto& content : contents) { - total_size += content.maximum_size; - } - - std::size_t count = 0; - int eta = -1; - - const auto initial_time = std::chrono::steady_clock::now(); - const auto UpdateETA = [total_size, &eta, initial_time](u64 size_imported) { - if (size_imported < 10 * 1024 * 1024) { // 10M Threshold - return; - } - using namespace std::chrono; - const u64 time_elapsed = - duration_cast(steady_clock::now() - initial_time).count(); - eta = - static_cast(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) { - emit NextContent(count + 1, wrapper.current_done_size + wrapper.current_pending_size, - content, eta); - if (!execute_func(importer, content, wrapper.Wrap(Callback))) { - if (!cancelled) { - failed_contents.emplace_back(content); - } - } - count++; - - if (cancelled) { - break; - } - } - emit Completed(); -} - -void MultiJob::Cancel() { - cancelled.store(true); - abort_func(importer); -} - -std::vector MultiJob::GetFailedContents() const { - return failed_contents; -} +// Copyright 2019 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include "frontend/helpers/multi_job.h" + +MultiJob::MultiJob(QObject* parent, Core::SDMCImporter& importer_, + std::vector contents_, ExecuteFunc execute_func_, + AbortFunc abort_func_) + : QThread(parent), importer(importer_), contents(std::move(contents_)), + execute_func(std::move(execute_func_)), abort_func(abort_func_) {} + +MultiJob::~MultiJob() = default; + +void MultiJob::run() { + u64 total_size = 0; + for (const auto& content : contents) { + total_size += content.maximum_size; + } + + std::size_t count = 0; + int eta = -1; + + const auto initial_time = std::chrono::steady_clock::now(); + const auto UpdateETA = [total_size, &eta, initial_time](u64 size_imported) { + if (size_imported < 10 * 1024 * 1024) { // 10M Threshold + return; + } + using namespace std::chrono; + const u64 time_elapsed = + duration_cast(steady_clock::now() - initial_time).count(); + eta = + static_cast(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) { + emit NextContent(count + 1, wrapper.current_done_size + wrapper.current_pending_size, + content, eta); + if (!execute_func(importer, content, wrapper.Wrap(Callback))) { + if (!cancelled) { + failed_contents.emplace_back(content); + } + } + count++; + + if (cancelled) { + break; + } + } + emit Completed(); +} + +void MultiJob::Cancel() { + cancelled.store(true); + abort_func(importer); +} + +std::vector MultiJob::GetFailedContents() const { + return failed_contents; +} diff --git a/src/frontend/helpers/multi_job.h b/src/frontend/helpers/multi_job.h index 1e602bc..ccf0c8f 100644 --- a/src/frontend/helpers/multi_job.h +++ b/src/frontend/helpers/multi_job.h @@ -1,56 +1,56 @@ -// Copyright 2019 threeSD Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include -#include -#include -#include "common/progress_callback.h" -#include "core/importer.h" - -class MultiJob : public QThread { - Q_OBJECT - -public: - using ExecuteFunc = std::function; - using AbortFunc = std::function; - - explicit MultiJob(QObject* parent, Core::SDMCImporter& importer, - std::vector contents, ExecuteFunc execute_func, - AbortFunc abort_func); - ~MultiJob() override; - - void run() override; - void Cancel(); - - std::vector GetFailedContents() const; - -signals: - /** - * Called when progress is updated on the current content. - * @param current_imported_size Imported size of the current content. - * @param total_imported_size Total imported size taking all previous contents into - * consideration. - * @param eta ETA in seconds, 0 when not determined. - */ - 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. - void NextContent(std::size_t count, u64 total_imported_size, - const Core::ContentSpecifier& next_content, int eta); - - void Completed(); - -private: - std::atomic_bool cancelled{false}; - Core::SDMCImporter& importer; - std::vector contents; - std::vector failed_contents; - ExecuteFunc execute_func; - AbortFunc abort_func; -}; - -Q_DECLARE_METATYPE(Core::ContentSpecifier) +// Copyright 2019 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include "common/progress_callback.h" +#include "core/importer.h" + +class MultiJob : public QThread { + Q_OBJECT + +public: + using ExecuteFunc = std::function; + using AbortFunc = std::function; + + explicit MultiJob(QObject* parent, Core::SDMCImporter& importer, + std::vector contents, ExecuteFunc execute_func, + AbortFunc abort_func); + ~MultiJob() override; + + void run() override; + void Cancel(); + + std::vector GetFailedContents() const; + +signals: + /** + * Called when progress is updated on the current content. + * @param current_imported_size Imported size of the current content. + * @param total_imported_size Total imported size taking all previous contents into + * consideration. + * @param eta ETA in seconds, 0 when not determined. + */ + 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. + void NextContent(std::size_t count, u64 total_imported_size, + const Core::ContentSpecifier& next_content, int eta); + + void Completed(); + +private: + std::atomic_bool cancelled{false}; + Core::SDMCImporter& importer; + std::vector contents; + std::vector failed_contents; + ExecuteFunc execute_func; + AbortFunc abort_func; +}; + +Q_DECLARE_METATYPE(Core::ContentSpecifier) diff --git a/src/frontend/helpers/rate_limited_progress_dialog.cpp b/src/frontend/helpers/rate_limited_progress_dialog.cpp index f3edc00..acf06dc 100644 --- a/src/frontend/helpers/rate_limited_progress_dialog.cpp +++ b/src/frontend/helpers/rate_limited_progress_dialog.cpp @@ -1,34 +1,34 @@ -// Copyright 2021 Pengfei Zhu -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include "frontend/helpers/rate_limited_progress_dialog.h" - -RateLimitedProgressDialog::RateLimitedProgressDialog(const QString& label_text, - const QString& cancel_button_text, int minimum, - int maximum, QWidget* parent) - : QProgressDialog(label_text, cancel_button_text, minimum, maximum, parent) { - - setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint)); - setWindowModality(Qt::WindowModal); - setMinimumDuration(0); - setValue(0); -} - -RateLimitedProgressDialog::~RateLimitedProgressDialog() = default; - -void RateLimitedProgressDialog::Update(int progress, const QString& label_text) { - if (progress == maximum()) { // always set the maximum - setValue(progress); - return; - } - - const auto current_time = std::chrono::steady_clock::now(); - if (current_time - last_update_time < MinimumInterval) { - return; - } - - setValue(progress); - setLabelText(label_text); - last_update_time = current_time; -} +// Copyright 2021 Pengfei Zhu +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "frontend/helpers/rate_limited_progress_dialog.h" + +RateLimitedProgressDialog::RateLimitedProgressDialog(const QString& label_text, + const QString& cancel_button_text, int minimum, + int maximum, QWidget* parent) + : QProgressDialog(label_text, cancel_button_text, minimum, maximum, parent) { + + setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint)); + setWindowModality(Qt::WindowModal); + setMinimumDuration(0); + setValue(0); +} + +RateLimitedProgressDialog::~RateLimitedProgressDialog() = default; + +void RateLimitedProgressDialog::Update(int progress, const QString& label_text) { + if (progress == maximum()) { // always set the maximum + setValue(progress); + return; + } + + const auto current_time = std::chrono::steady_clock::now(); + if (current_time - last_update_time < MinimumInterval) { + return; + } + + setValue(progress); + setLabelText(label_text); + last_update_time = current_time; +} diff --git a/src/frontend/helpers/rate_limited_progress_dialog.h b/src/frontend/helpers/rate_limited_progress_dialog.h index 1120b78..11988a5 100644 --- a/src/frontend/helpers/rate_limited_progress_dialog.h +++ b/src/frontend/helpers/rate_limited_progress_dialog.h @@ -1,21 +1,21 @@ -// Copyright 2021 Pengfei Zhu -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include -#include - -class RateLimitedProgressDialog : public QProgressDialog { -public: - explicit RateLimitedProgressDialog(const QString& label_text, const QString& cancel_button_text, - int minimum, int maximum, QWidget* parent = nullptr); - ~RateLimitedProgressDialog() override; - - void Update(int progress, const QString& label_text); - -private: - std::chrono::steady_clock::time_point last_update_time = std::chrono::steady_clock::now(); - static constexpr auto MinimumInterval = std::chrono::milliseconds{100}; -}; +// Copyright 2021 Pengfei Zhu +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include + +class RateLimitedProgressDialog : public QProgressDialog { +public: + explicit RateLimitedProgressDialog(const QString& label_text, const QString& cancel_button_text, + int minimum, int maximum, QWidget* parent = nullptr); + ~RateLimitedProgressDialog() override; + + void Update(int progress, const QString& label_text); + +private: + std::chrono::steady_clock::time_point last_update_time = std::chrono::steady_clock::now(); + static constexpr auto MinimumInterval = std::chrono::milliseconds{100}; +}; diff --git a/src/frontend/helpers/simple_job.cpp b/src/frontend/helpers/simple_job.cpp index 26b49e5..be49dee 100644 --- a/src/frontend/helpers/simple_job.cpp +++ b/src/frontend/helpers/simple_job.cpp @@ -1,54 +1,54 @@ -// Copyright 2020 Pengfei Zhu -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include -#include "frontend/helpers/frontend_common.h" -#include "frontend/helpers/rate_limited_progress_dialog.h" -#include "frontend/helpers/simple_job.h" - -SimpleJob::SimpleJob(QObject* parent, ExecuteFunc execute_, AbortFunc abort_) - : QThread(parent), execute(std::move(execute_)), abort(std::move(abort_)) {} - -SimpleJob::~SimpleJob() = default; - -void SimpleJob::run() { - const bool ret = - execute([this](u64 current, u64 total) { emit ProgressUpdated(current, total); }); - - if (ret || canceled) { - emit Completed(canceled); - } else { - emit ErrorOccured(); - } -} - -void SimpleJob::Cancel() { - canceled = true; - abort(); -} - -void SimpleJob::StartWithProgressDialog(QWidget* widget) { - auto* dialog = new RateLimitedProgressDialog(tr("Initializing..."), tr("Cancel"), 0, 0, widget); - connect(this, &SimpleJob::ProgressUpdated, this, [dialog](u64 current, u64 total) { - if (dialog->wasCanceled()) { - return; - } - // Try to map total to int range - // This is equal to ceil(total / INT_MAX) - const u64 multiplier = - (total + std::numeric_limits::max() - 1) / std::numeric_limits::max(); - dialog->setMaximum(static_cast(total / multiplier)); - dialog->Update(static_cast(current / multiplier), - tr("%1 / %2").arg(ReadableByteSize(current), ReadableByteSize(total))); - }); - connect(this, &SimpleJob::ErrorOccured, this, [widget, dialog] { - QMessageBox::critical(widget, tr("threeSD"), - tr("Operation failed. Please refer to the log.")); - dialog->hide(); - }); - connect(this, &SimpleJob::Completed, dialog, &QProgressDialog::hide); - connect(dialog, &QProgressDialog::canceled, this, &SimpleJob::Cancel); - - start(); -} +// Copyright 2020 Pengfei Zhu +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include "frontend/helpers/frontend_common.h" +#include "frontend/helpers/rate_limited_progress_dialog.h" +#include "frontend/helpers/simple_job.h" + +SimpleJob::SimpleJob(QObject* parent, ExecuteFunc execute_, AbortFunc abort_) + : QThread(parent), execute(std::move(execute_)), abort(std::move(abort_)) {} + +SimpleJob::~SimpleJob() = default; + +void SimpleJob::run() { + const bool ret = + execute([this](u64 current, u64 total) { emit ProgressUpdated(current, total); }); + + if (ret || canceled) { + emit Completed(canceled); + } else { + emit ErrorOccured(); + } +} + +void SimpleJob::Cancel() { + canceled = true; + abort(); +} + +void SimpleJob::StartWithProgressDialog(QWidget* widget) { + auto* dialog = new RateLimitedProgressDialog(tr("Initializing..."), tr("Cancel"), 0, 0, widget); + connect(this, &SimpleJob::ProgressUpdated, this, [dialog](u64 current, u64 total) { + if (dialog->wasCanceled()) { + return; + } + // Try to map total to int range + // This is equal to ceil(total / INT_MAX) + const u64 multiplier = + (total + std::numeric_limits::max() - 1) / std::numeric_limits::max(); + dialog->setMaximum(static_cast(total / multiplier)); + dialog->Update(static_cast(current / multiplier), + tr("%1 / %2").arg(ReadableByteSize(current), ReadableByteSize(total))); + }); + connect(this, &SimpleJob::ErrorOccured, this, [widget, dialog] { + QMessageBox::critical(widget, tr("threeSD"), + tr("Operation failed. Please refer to the log.")); + dialog->hide(); + }); + connect(this, &SimpleJob::Completed, dialog, &QProgressDialog::hide); + connect(dialog, &QProgressDialog::canceled, this, &SimpleJob::Cancel); + + start(); +} diff --git a/src/frontend/helpers/simple_job.h b/src/frontend/helpers/simple_job.h index e39ee6d..b2bad1e 100644 --- a/src/frontend/helpers/simple_job.h +++ b/src/frontend/helpers/simple_job.h @@ -1,39 +1,39 @@ -// Copyright 2019 threeSD Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include -#include -#include "common/common_types.h" -#include "common/progress_callback.h" - -/** - * Lightweight wrapper around QThread, for easy use with progressive jobs. - */ -class SimpleJob : public QThread { - Q_OBJECT - -public: - using ExecuteFunc = std::function; - using AbortFunc = std::function; - - explicit SimpleJob(QObject* parent, ExecuteFunc execute, AbortFunc abort); - ~SimpleJob() override; - - void run() override; - void Cancel(); - - void StartWithProgressDialog(QWidget* widget); - -signals: - void ProgressUpdated(u64 current, u64 total); - void Completed(bool canceled); - void ErrorOccured(); - -private: - ExecuteFunc execute; - AbortFunc abort; - bool canceled{}; -}; +// Copyright 2019 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include "common/common_types.h" +#include "common/progress_callback.h" + +/** + * Lightweight wrapper around QThread, for easy use with progressive jobs. + */ +class SimpleJob : public QThread { + Q_OBJECT + +public: + using ExecuteFunc = std::function; + using AbortFunc = std::function; + + explicit SimpleJob(QObject* parent, ExecuteFunc execute, AbortFunc abort); + ~SimpleJob() override; + + void run() override; + void Cancel(); + + void StartWithProgressDialog(QWidget* widget); + +signals: + void ProgressUpdated(u64 current, u64 total); + void Completed(bool canceled); + void ErrorOccured(); + +private: + ExecuteFunc execute; + AbortFunc abort; + bool canceled{}; +}; diff --git a/src/frontend/import_dialog.cpp b/src/frontend/import_dialog.cpp index d2c6c3c..0182365 100644 --- a/src/frontend/import_dialog.cpp +++ b/src/frontend/import_dialog.cpp @@ -1,839 +1,839 @@ -// Copyright 2019 threeSD Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "common/assert.h" -#include "common/logging/log.h" -#include "common/progress_callback.h" -#include "common/scope_exit.h" -#include "frontend/cia_build_dialog.h" -#include "frontend/helpers/frontend_common.h" -#include "frontend/helpers/multi_job.h" -#include "frontend/helpers/rate_limited_progress_dialog.h" -#include "frontend/helpers/simple_job.h" -#include "frontend/import_dialog.h" -#include "frontend/title_info_dialog.h" -#include "ui_import_dialog.h" - -// content type, singular name, plural name, icon name -// clang-format off -static constexpr std::array, 9> - ContentTypeMap{{ - {Core::ContentType::Application, QT_TR_NOOP("Application"), QT_TR_NOOP("Applications"), "app"}, - {Core::ContentType::Update, QT_TR_NOOP("Update"), QT_TR_NOOP("Updates"), "update"}, - {Core::ContentType::DLC, QT_TR_NOOP("DLC"), QT_TR_NOOP("DLCs"), "dlc"}, - {Core::ContentType::Savegame, QT_TR_NOOP("Save Data"), QT_TR_NOOP("Save Data"), "save_data"}, - {Core::ContentType::Extdata, QT_TR_NOOP("Extra Data"), QT_TR_NOOP("Extra Data"), "save_data"}, - {Core::ContentType::SystemArchive, QT_TR_NOOP("System Archive"), QT_TR_NOOP("System Archives"), "system_archive"}, - {Core::ContentType::Sysdata, QT_TR_NOOP("System Data"), QT_TR_NOOP("System Data"), "system_data"}, - {Core::ContentType::SystemTitle, QT_TR_NOOP("System Title"), QT_TR_NOOP("System Titles"), "hos"}, - {Core::ContentType::SystemApplet, QT_TR_NOOP("System Applet"), QT_TR_NOOP("System Applets"), "hos"}, - }}; -// clang-format on - -static QString GetContentName(const Core::ContentSpecifier& specifier) { - return specifier.name.empty() - ? QStringLiteral("0x%1").arg(specifier.id, 16, 16, QLatin1Char('0')) - : QString::fromStdString(specifier.name); -} - -template -static QString GetContentTypeName(Core::ContentType type) { - if constexpr (Plural) { - return QObject::tr(std::get<2>(ContentTypeMap.at(static_cast(type))), - "ImportDialog"); - } else { - return QObject::tr(std::get<1>(ContentTypeMap.at(static_cast(type))), - "ImportDialog"); - } -} - -static QPixmap GetContentTypeIcon(Core::ContentType type) { - return QIcon::fromTheme( - QString::fromUtf8(std::get<3>(ContentTypeMap.at(static_cast(type))))) - .pixmap(24); -} - -static QPixmap GetContentIcon(const Core::ContentSpecifier& specifier, - bool use_category_icon = false) { - if (specifier.icon.empty()) { - // Return a category icon, or a null icon - return use_category_icon ? GetContentTypeIcon(specifier.type) - : QIcon::fromTheme(QStringLiteral("unknown")).pixmap(24); - } - return QPixmap::fromImage(QImage(reinterpret_cast(specifier.icon.data()), 24, 24, - QImage::Format::Format_RGB16)); -} - -ImportDialog::ImportDialog(QWidget* parent, const Core::Config& config_) - : DPIAwareDialog(parent, 560, 320), ui(std::make_unique()), config(config_) { - - qRegisterMetaType("u64"); - qRegisterMetaType("std::size_t"); - qRegisterMetaType(); - - ui->setupUi(this); - setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint)); - - RelistContent(); - UpdateSizeDisplay(); - - ui->title_view_button->setChecked(true); - - ui->buttonBox->button(QDialogButtonBox::StandardButton::Reset)->setText(tr("Refresh")); - connect(ui->buttonBox, &QDialogButtonBox::clicked, [this](QAbstractButton* button) { - if (button == ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)) { - StartImporting(); - } else if (button == ui->buttonBox->button(QDialogButtonBox::StandardButton::Cancel)) { - reject(); - } else { - RelistContent(); - } - }); - - connect(ui->title_view_button, &QRadioButton::toggled, this, &ImportDialog::RepopulateContent); - connect(ui->advanced_button, &QPushButton::clicked, this, &ImportDialog::ShowAdvancedMenu); - - ui->main->sortByColumn(-1, Qt::AscendingOrder); // disable sorting by default - ui->main->header()->setStretchLastSection(false); - connect(ui->main, &QTreeWidget::customContextMenuRequested, this, &ImportDialog::OnContextMenu); -} - -ImportDialog::~ImportDialog() = default; - -void ImportDialog::SetContentSizes(int previous_width, int previous_height) { - const int current_width = width(); - if (previous_width == 0) { // first time - ui->main->setColumnWidth(0, current_width * 0.66); - ui->main->setColumnWidth(1, current_width * 0.145); - ui->main->setColumnWidth(2, current_width * 0.10); - } else { // proportionally update column widths - for (int i : {0, 1, 2}) { - ui->main->setColumnWidth(i, ui->main->columnWidth(i) * current_width / previous_width); - } - } -} - -void ImportDialog::RelistContent() { - auto* dialog = - new RateLimitedProgressDialog(tr("Loading Contents..."), tr("Cancel"), 0, 0, this); - dialog->setCancelButton(nullptr); - - using FutureWatcher = QFutureWatcher; - auto* future_watcher = new FutureWatcher(this); - connect(future_watcher, &FutureWatcher::finished, this, [this, dialog] { - dialog->hide(); - if (importer->IsGood()) { - 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( - [&importer = this->importer, &config = this->config, &contents = this->contents] { - if (!importer) { - importer = std::make_unique(config); - } - if (importer->IsGood()) { - contents = importer->ListContent(); - } - }); - future_watcher->setFuture(future); -} - -constexpr Qt::ItemDataRole SpecifierIndexRole = Qt::UserRole; - -/// Supports readable size display and sorting -class ContentListItem final : public QTreeWidgetItem { -public: - explicit ContentListItem(QString name, u64 content_size_, QString exists) - : QTreeWidgetItem{{std::move(name), ReadableByteSize(content_size_), std::move(exists)}}, - content_size(content_size_) {} - - explicit ContentListItem(QString name, u64 content_size_, QString exists, std::size_t idx) - : QTreeWidgetItem{{std::move(name), ReadableByteSize(content_size_), std::move(exists)}}, - content_size(content_size_) { - setData(0, SpecifierIndexRole, static_cast(idx)); - } - ~ContentListItem() override = default; - -private: - bool operator<(const QTreeWidgetItem& other_item) const override { - const auto* other = dynamic_cast(&other_item); - if (!other) { - return false; - } - - const int column = treeWidget()->sortColumn(); - if (column == 1) { // size - return content_size < other->content_size; - } else { - return text(column) < other->text(column); - } - } - - u64 content_size; -}; - -void ImportDialog::InsertTopLevelItem(QString text, QPixmap icon) { - auto* item = new QTreeWidgetItem{{text}}; - item->setIcon(0, QIcon(std::move(icon))); - - item->setFlags(item->flags() | Qt::ItemIsAutoTristate); - item->setCheckState(0, Qt::Unchecked); // required to give the item a checkbox - - ui->main->invisibleRootItem()->addChild(item); - item->setFirstColumnSpanned(true); -} - -void ImportDialog::InsertTopLevelItem(QString text, QPixmap icon, u64 total_size, QString exists) { - auto* item = new ContentListItem{std::move(text), total_size, std::move(exists)}; - item->setIcon(0, QIcon(std::move(icon))); - - item->setFlags(item->flags() | Qt::ItemIsAutoTristate); - item->setCheckState(0, Qt::Unchecked); // required to give the item a checkbox - - ui->main->invisibleRootItem()->addChild(item); -} - -// Content types that themselves form a 'Title' like entity. -constexpr std::array SpecialContentTypeList{{ - Core::ContentType::SystemArchive, - Core::ContentType::Sysdata, - Core::ContentType::SystemTitle, - Core::ContentType::SystemApplet, -}}; - -void ImportDialog::InsertSecondLevelItem(std::size_t row, const Core::ContentSpecifier& content, - std::size_t id, QString replace_name, - QPixmap replace_icon) { - const bool use_title_view = ui->title_view_button->isChecked(); - - QString name; - if (use_title_view) { - if (row == 0) { - name = QStringLiteral("%1 (%2)") - .arg(GetContentName(content)) - .arg(GetContentTypeName(content.type)); - } else if (row <= SpecialContentTypeList.size()) { - name = GetContentName(content); - } else { - name = GetContentTypeName(content.type); - } - } else { - name = GetContentName(content); - } - - if (!replace_name.isEmpty()) { - name = replace_name; - } - - auto* item = new ContentListItem{name, content.maximum_size, - content.already_exists ? tr("Yes") : tr("No"), id}; - - // Set icon - QPixmap icon; - if (replace_icon.isNull()) { - // Exclude system titles, they are a single group but have own icons. - if (use_title_view && content.type != Core::ContentType::SystemTitle && - content.type != Core::ContentType::SystemApplet) { - icon = GetContentTypeIcon(content.type); - } else { - // When not in title view, System Data and System Archive groups use category icons. - const bool use_category_icon = content.type == Core::ContentType::Sysdata || - content.type == Core::ContentType::SystemArchive; - icon = GetContentIcon(content, use_category_icon); - } - } else { - icon = replace_icon; - } - item->setIcon(0, QIcon(icon)); - - // Skip System Applets, but enable everything else by default. - if (!content.already_exists && content.type != Core::ContentType::SystemApplet) { - item->setCheckState(0, Qt::Checked); - total_selected_size += content.maximum_size; - } else { - item->setCheckState(0, Qt::Unchecked); - } - - ui->main->invisibleRootItem()->child(row)->addChild(item); -} - -void ImportDialog::OnItemChanged(QTreeWidgetItem* item, int column) { - // Only handle second level items (with checkboxes) - if (column != 0 || !item->parent()) { - return; - } - - const auto& specifier = SpecifierFromItem(item); - if (item->checkState(0) == Qt::Checked) { - if (!applet_warning_shown && !specifier.already_exists && - specifier.type == Core::ContentType::SystemApplet) { - - QMessageBox::warning( - this, tr("Warning"), - tr("You are trying to import System Applets.\nThese are known to cause problems " - "with certain games.\nOnly proceed if you understand what you are doing.")); - applet_warning_shown = true; - } - total_selected_size += specifier.maximum_size; - } else { - if (!system_warning_shown && !specifier.already_exists && - (specifier.type == Core::ContentType::SystemArchive || - specifier.type == Core::ContentType::Sysdata || - specifier.type == Core::ContentType::SystemTitle)) { - - QMessageBox::warning(this, tr("Warning"), - tr("You are de-selecting important files that may be necessary " - "for your imported games to run.\nIt is highly recommended to " - "import these contents if they do not exist yet.")); - system_warning_shown = true; - } - total_selected_size -= specifier.maximum_size; - } - UpdateSizeDisplay(); -} - -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; - ui->main->clear(); - ui->main->setSortingEnabled(false); - disconnect(ui->main, &QTreeWidget::itemChanged, this, &ImportDialog::OnItemChanged); - - struct TitleMapEntry { - QString name; - QPixmap icon; - std::vector contents; - }; - std::map title_map; - std::unordered_map extdata_id_map; // extdata ID -> title ID - for (const auto& content : contents) { - if (content.type == Core::ContentType::Application) { - title_map[content.id].name = GetContentName(content); - title_map[content.id].icon = GetContentIcon(content); - extdata_id_map.emplace(content.extdata_id, content.id); - } - } - for (const auto& content : contents) { - if (content.type == Core::ContentType::Extdata) { - if (extdata_id_map.count(content.id)) { - const u64 title_id = extdata_id_map.at(content.id); - title_map[title_id].contents.emplace_back(&content); - } - } else if (content.type == Core::ContentType::Application || - content.type == Core::ContentType::Update || - content.type == Core::ContentType::DLC || - content.type == Core::ContentType::Savegame) { - if (title_map.count(content.id)) { - title_map[content.id].contents.emplace_back(&content); - } - } - } - - const bool use_title_view = ui->title_view_button->isChecked(); - if (use_title_view) { - // Create 'Ungrouped' category. - InsertTopLevelItem(tr("Ungrouped"), QIcon::fromTheme(QStringLiteral("unknown")).pixmap(24)); - - // Create categories for special content types. - for (std::size_t i = 0; i < SpecialContentTypeList.size(); ++i) { - InsertTopLevelItem(GetContentTypeName(SpecialContentTypeList[i]), - GetContentTypeIcon(SpecialContentTypeList[i])); - } - - // Titles - std::unordered_map title_row_map; - for (auto& [id, entry] : title_map) { - // Process the title's contents - u64 total_size = 0; - bool has_exist = false, has_non_exist = false; - for (const auto* content : entry.contents) { - total_size += content->maximum_size; - if (content->already_exists) { - has_exist = true; - } else { - has_non_exist = true; - } - } - - QString exist_text; - if (!has_exist) { - exist_text = tr("No"); - } else if (!has_non_exist) { - exist_text = tr("Yes"); - } else { - exist_text = tr("Part"); - } - - InsertTopLevelItem(std::move(entry.name), std::move(entry.icon), total_size, - std::move(exist_text)); - title_row_map.emplace(id, ui->main->invisibleRootItem()->childCount() - 1); - } - - for (std::size_t i = 0; i < contents.size(); ++i) { - const auto& content = contents[i]; - - std::size_t row = 0; // 0 for ungrouped (default) - switch (content.type) { - case Core::ContentType::Application: - case Core::ContentType::Update: - case Core::ContentType::DLC: - case Core::ContentType::Savegame: { - // Fix the id - const auto real_id = content.id & 0xffffff00ffffffff; - row = title_row_map.count(real_id) ? title_row_map.at(real_id) : 0; - break; - } - case Core::ContentType::Extdata: { - if (extdata_id_map.count(content.id)) { - row = title_row_map.at(extdata_id_map.at(content.id)); - } else { - row = 0; // Ungrouped - } - break; - } - default: { - const std::size_t idx = std::find(SpecialContentTypeList.begin(), - SpecialContentTypeList.end(), content.type) - - SpecialContentTypeList.begin(); - ASSERT_MSG(idx < SpecialContentTypeList.size(), "Content Type not handled"); - row = idx + 1; - break; - } - } - - InsertSecondLevelItem(row, content, i); - } - } else { - for (const auto& [type, singular_name, plural_name, icon_name] : ContentTypeMap) { - InsertTopLevelItem(tr(plural_name), GetContentTypeIcon(type)); - } - - for (std::size_t i = 0; i < contents.size(); ++i) { - const auto& content = contents[i]; - - QString name{}; - QPixmap icon{}; - if (content.type == Core::ContentType::Savegame) { - if (title_map.count(content.id)) { - name = title_map.at(content.id).name; - icon = title_map.at(content.id).icon; - } - } else if (content.type == Core::ContentType::Extdata) { - if (extdata_id_map.count(content.id)) { - u64 title_id = extdata_id_map.at(content.id); - name = title_map.at(title_id).name; - icon = title_map.at(title_id).icon; - } - } - - InsertSecondLevelItem(static_cast(content.type), content, i, name, icon); - } - } - - ui->main->setSortingEnabled(true); - connect(ui->main, &QTreeWidget::itemChanged, this, &ImportDialog::OnItemChanged); - UpdateSizeDisplay(); -} - -void ImportDialog::UpdateSizeDisplay() { - QStorageInfo storage(QString::fromStdString(config.user_path)); - if (!storage.isValid() || !storage.isReady()) { - LOG_ERROR(Frontend, "Storage {} is not good", config.user_path); - QMessageBox::critical( - this, tr("Bad Storage"), - tr("An error occured while trying to get available space for the storage.")); - reject(); - return; - } - - ui->availableSpace->setText( - tr("Available Space: %1").arg(ReadableByteSize(storage.bytesAvailable()))); - ui->totalSize->setText(tr("Total Size: %1").arg(ReadableByteSize(total_selected_size))); - - ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok) - ->setEnabled(total_selected_size > 0 && - total_selected_size <= static_cast(storage.bytesAvailable())); -} - -std::vector ImportDialog::GetSelectedContentList() { - std::vector to_import; - for (int i = 0; i < ui->main->invisibleRootItem()->childCount(); ++i) { - const auto* item = ui->main->invisibleRootItem()->child(i); - for (int j = 0; j < item->childCount(); ++j) { - if (item->child(j)->checkState(0) == Qt::Checked) { - to_import.emplace_back( - contents[item->child(j)->data(0, SpecifierIndexRole).toInt()]); - } - } - } - - return to_import; -} - -Core::ContentSpecifier ImportDialog::SpecifierFromItem(QTreeWidgetItem* item) const { - return contents[item->data(0, SpecifierIndexRole).toInt()]; -} - -void ImportDialog::OnContextMenu(const QPoint& point) { - QTreeWidgetItem* item = ui->main->itemAt(point.x(), point.y()); - if (!item) { - return; - } - - const bool title_view = ui->title_view_button->isChecked(); - - QMenu context_menu(this); - if (item->parent()) { // Second level - const auto& specifier = SpecifierFromItem(item); - if (specifier.type == Core::ContentType::Application) { - context_menu.addAction(tr("Dump CXI file"), - [this, specifier] { StartDumpingCXISingle(specifier); }); - } - if (Core::IsTitle(specifier.type)) { - context_menu.addAction(tr("Build CIA..."), - [this, specifier] { StartBuildingCIASingle(specifier); }); - context_menu.addAction(tr("Show Title Info"), [this, specifier] { - TitleInfoDialog dialog(this, *importer, specifier); - dialog.exec(); - }); - } - } else { // Top level - if (!title_view) { - return; - } - - for (int i = 0; i < item->childCount(); ++i) { - const auto& specifier = SpecifierFromItem(item->child(i)); - if (specifier.type == Core::ContentType::Application) { - context_menu.addAction(tr("Dump Base CXI file"), - [this, specifier] { StartDumpingCXISingle(specifier); }); - context_menu.addAction(tr("Build Base CIA"), - [this, specifier] { StartBuildingCIASingle(specifier); }); - } else if (specifier.type == Core::ContentType::Update) { - context_menu.addAction(tr("Build Update CIA"), - [this, specifier] { StartBuildingCIASingle(specifier); }); - } else if (specifier.type == Core::ContentType::DLC) { - context_menu.addAction(tr("Build DLC CIA"), - [this, specifier] { StartBuildingCIASingle(specifier); }); - } - } - } - context_menu.exec(ui->main->viewport()->mapToGlobal(point)); -} - -class AdvancedMenu : public QMenu { -public: - explicit AdvancedMenu(QWidget* parent) : QMenu(parent) {} - -private: - void mousePressEvent(QMouseEvent* event) override { - auto* dialog = static_cast(parentWidget()); - // Block popup menu when clicking on the Advanced button to dismiss the menu. - // With out this, it will immediately bring up the menu again. - if (dialog->childAt(dialog->mapFromGlobal(event->globalPos())) == - dialog->ui->advanced_button) { - - dialog->block_advanced_menu = true; - } - - QMenu::mousePressEvent(event); - } -}; - -void ImportDialog::ShowAdvancedMenu() { - if (block_advanced_menu) { - block_advanced_menu = false; - return; - } - - AdvancedMenu menu(this); - menu.addAction(tr("Batch Dump CXI"), this, &ImportDialog::StartBatchDumpingCXI); - menu.addAction(tr("Batch Build CIA"), this, &ImportDialog::StartBatchBuildingCIA); - - menu.exec(ui->advanced_button->mapToGlobal(ui->advanced_button->rect().bottomLeft())); -} - -static QString FormatETA(int eta) { - if (eta < 0) { - return QStringLiteral(" "); - } - return QCoreApplication::translate("ImportDialog", "ETA %1m%2s") - .arg(eta / 60, 2, 10, QLatin1Char('0')) - .arg(eta % 60, 2, 10, QLatin1Char('0')); -} - -// Runs the job, opening a dialog to report is progress. -void ImportDialog::RunMultiJob(MultiJob* job, std::size_t total_count, u64 total_size) { - // Try to map total_size to int range - // This is equal to ceil(total_size / INT_MAX) - const u64 multiplier = - (total_size + std::numeric_limits::max() - 1) / std::numeric_limits::max(); - - auto* label = new QLabel(tr("Initializing...")); - label->setWordWrap(true); - label->setFixedWidth(600); - - auto* dialog = new RateLimitedProgressDialog(tr("Initializing..."), tr("Cancel"), 0, - static_cast(total_size / multiplier), this); - dialog->setLabel(label); - - connect(job, &MultiJob::NextContent, this, - [this, dialog, multiplier, total_count](std::size_t count, u64 total_imported_size, - const Core::ContentSpecifier& next_content, - int eta) { - if (dialog->wasCanceled()) { - return; - } - dialog->Update(static_cast(total_imported_size / multiplier), - tr("

(%1/%2) %3 (%4)

 

%5

") - .arg(count) - .arg(total_count) - .arg(GetContentName(next_content)) - .arg(GetContentTypeName(next_content.type)) - .arg(FormatETA(eta))); - current_content = next_content; - current_count = count; - }); - connect( - job, &MultiJob::ProgressUpdated, this, - [this, dialog, multiplier, total_count](u64 current_imported_size, u64 total_imported_size, - int eta) { - if (dialog->wasCanceled()) { - return; - } - dialog->Update( - static_cast(total_imported_size / multiplier), - tr("

(%1/%2) %3 (%4)

%5 / %6

%7

") - .arg(current_count) - .arg(total_count) - .arg(GetContentName(current_content)) - .arg(GetContentTypeName(current_content.type)) - .arg(ReadableByteSize(current_imported_size)) - .arg(ReadableByteSize(current_content.maximum_size)) - .arg(FormatETA(eta))); - }); - connect(job, &MultiJob::Completed, this, [this, dialog, job] { - dialog->hide(); - - const auto failed_contents = job->GetFailedContents(); - if (failed_contents.empty()) { - QMessageBox::information(this, tr("threeSD"), tr("All contents done successfully.")); - } else { - QString list_content; - for (const auto& content : failed_contents) { - list_content.append(QStringLiteral("
  • %1 (%2)
  • ") - .arg(GetContentName(content)) - .arg(GetContentTypeName(content.type))); - } - QMessageBox::critical(this, tr("threeSD"), - tr("List of failed contents:
      %1
    ").arg(list_content)); - } - - RelistContent(); - }); - connect(dialog, &QProgressDialog::canceled, this, [this, job] { - // Add yet-another-ProgressDialog to indicate cancel progress - auto* cancel_dialog = - new RateLimitedProgressDialog(tr("Canceling..."), tr("Cancel"), 0, 0, this); - cancel_dialog->setCancelButton(nullptr); - connect(job, &MultiJob::Completed, cancel_dialog, &QProgressDialog::hide); - job->Cancel(); - }); - - job->start(); -} - -void ImportDialog::StartImporting() { - UpdateSizeDisplay(); - if (!ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)->isEnabled()) { - // Space is no longer enough - QMessageBox::warning(this, tr("Not Enough Space"), - tr("Your disk does not have enough space to hold imported data.")); - return; - } - - auto to_import = GetSelectedContentList(); - const std::size_t total_count = to_import.size(); - - auto* job = - new MultiJob(this, *importer, std::move(to_import), &Core::SDMCImporter::ImportContent, - &Core::SDMCImporter::AbortImporting); - - RunMultiJob(job, total_count, total_selected_size); -} - -// CXI dumping - -void ImportDialog::StartDumpingCXISingle(const Core::ContentSpecifier& specifier) { - const QString path = QFileDialog::getSaveFileName(this, tr("Dump CXI file"), last_dump_cxi_path, - tr("CTR Executable Image (*.cxi)")); - if (path.isEmpty()) { - return; - } - last_dump_cxi_path = QFileInfo(path).path(); - - auto* job = new SimpleJob( - this, - [this, specifier, path](const Common::ProgressCallback& callback) { - return importer->DumpCXI(specifier, path.toStdString(), callback); - }, - [this] { importer->AbortDumpCXI(); }); - job->StartWithProgressDialog(this); -} - -void ImportDialog::StartBatchDumpingCXI() { - auto to_import = GetSelectedContentList(); - if (to_import.empty()) { - QMessageBox::warning(this, tr("threeSD"), - tr("Please select the contents you would like to dump as CXIs.")); - return; - } - - const auto removed_iter = std::remove_if( - to_import.begin(), to_import.end(), [](const Core::ContentSpecifier& specifier) { - return specifier.type != Core::ContentType::Application; - }); - if (removed_iter == to_import.begin()) { // No Applications selected - QMessageBox::critical(this, tr("threeSD"), - tr("The contents selected are not supported.
    You can only dump " - "Applications as CXIs.")); - return; - } - if (removed_iter != to_import.end()) { // Some non-Applications selected - QMessageBox::warning(this, tr("threeSD"), - tr("Some contents selected are not supported and will be " - "ignored.
    Only Applications will be dumped as CXIs.")); - } - - to_import.erase(removed_iter, to_import.end()); - - QString path = - QFileDialog::getExistingDirectory(this, tr("Batch Dump CXI"), last_batch_dump_cxi_path); - if (path.isEmpty()) { - return; - } - last_batch_dump_cxi_path = path; - if (!path.endsWith(QChar::fromLatin1('/')) && !path.endsWith(QChar::fromLatin1('\\'))) { - path.append(QStringLiteral("/")); - } - - const auto total_count = to_import.size(); - const auto total_size = std::accumulate(to_import.begin(), to_import.end(), u64{0}, - [](u64 sum, const Core::ContentSpecifier& specifier) { - return sum + specifier.maximum_size; - }); - auto* job = new MultiJob( - this, *importer, std::move(to_import), - [path](Core::SDMCImporter& importer, const Core::ContentSpecifier& specifier, - const Common::ProgressCallback& callback) { - return importer.DumpCXI(specifier, path.toStdString(), callback, true); - }, - &Core::SDMCImporter::AbortDumpCXI); - RunMultiJob(job, total_count, total_size); -} - -// CIA building - -void ImportDialog::StartBuildingCIASingle(const Core::ContentSpecifier& specifier) { - CIABuildDialog dialog(this, - /*is_dir*/ false, - /*is_nand*/ Core::IsNandTitle(specifier.type), - /*enable_legit*/ importer->CanBuildLegitCIA(specifier), - last_build_cia_path); - if (dialog.exec() != QDialog::Accepted) { - return; - } - const auto& [path, type] = dialog.GetResults(); - last_build_cia_path = QFileInfo(path).path(); - - auto* job = new SimpleJob( - this, - [this, specifier, path = path, type = type](const Common::ProgressCallback& callback) { - return importer->BuildCIA(type, specifier, path.toStdString(), callback); - }, - [this] { importer->AbortBuildCIA(); }); - job->StartWithProgressDialog(this); -} - -void ImportDialog::StartBatchBuildingCIA() { - auto to_import = GetSelectedContentList(); - if (to_import.empty()) { - QMessageBox::warning(this, tr("threeSD"), - tr("Please select the contents you would like to build as CIAs.")); - return; - } - - const auto removed_iter = std::remove_if( - to_import.begin(), to_import.end(), - [](const Core::ContentSpecifier& specifier) { return !Core::IsTitle(specifier.type); }); - if (removed_iter == to_import.begin()) { // No Titles selected - QMessageBox::critical(this, tr("threeSD"), - tr("The contents selected are not supported.
    You can only build " - "CIAs from Applications, Updates, DLCs and System Titles.")); - return; - } - if (removed_iter != to_import.end()) { // Some non-Titles selected - QMessageBox::warning( - this, tr("threeSD"), - tr("Some contents selected are not supported and will be ignored.
    Only " - "Applications, Updates, DLCs and System Titles will be built as CIAs.")); - } - - to_import.erase(removed_iter, to_import.end()); - - const bool is_nand = std::all_of( - to_import.begin(), to_import.end(), - [](const Core::ContentSpecifier& specifier) { return Core::IsNandTitle(specifier.type); }); - const bool enable_legit = std::all_of(to_import.begin(), to_import.end(), - [this](const Core::ContentSpecifier& specifier) { - return importer->CanBuildLegitCIA(specifier); - }); - CIABuildDialog dialog(this, /*is_dir*/ true, is_nand, enable_legit, last_batch_build_cia_path); - if (dialog.exec() != QDialog::Accepted) { - return; - } - auto [path, type] = dialog.GetResults(); - last_batch_build_cia_path = path; - if (!path.endsWith(QChar::fromLatin1('/')) && !path.endsWith(QChar::fromLatin1('\\'))) { - path.append(QStringLiteral("/")); - } - - const auto total_count = to_import.size(); - const auto total_size = std::accumulate(to_import.begin(), to_import.end(), u64{0}, - [](u64 sum, const Core::ContentSpecifier& specifier) { - return sum + specifier.maximum_size; - }); - auto* job = new MultiJob( - this, *importer, std::move(to_import), - [path = path, type = type](Core::SDMCImporter& importer, - const Core::ContentSpecifier& specifier, - const Common::ProgressCallback& callback) { - return importer.BuildCIA(type, specifier, path.toStdString(), callback, true); - }, - &Core::SDMCImporter::AbortBuildCIA); - RunMultiJob(job, total_count, total_size); -} +// Copyright 2019 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "common/assert.h" +#include "common/logging/log.h" +#include "common/progress_callback.h" +#include "common/scope_exit.h" +#include "frontend/cia_build_dialog.h" +#include "frontend/helpers/frontend_common.h" +#include "frontend/helpers/multi_job.h" +#include "frontend/helpers/rate_limited_progress_dialog.h" +#include "frontend/helpers/simple_job.h" +#include "frontend/import_dialog.h" +#include "frontend/title_info_dialog.h" +#include "ui_import_dialog.h" + +// content type, singular name, plural name, icon name +// clang-format off +static constexpr std::array, 9> + ContentTypeMap{{ + {Core::ContentType::Application, QT_TR_NOOP("Application"), QT_TR_NOOP("Applications"), "app"}, + {Core::ContentType::Update, QT_TR_NOOP("Update"), QT_TR_NOOP("Updates"), "update"}, + {Core::ContentType::DLC, QT_TR_NOOP("DLC"), QT_TR_NOOP("DLCs"), "dlc"}, + {Core::ContentType::Savegame, QT_TR_NOOP("Save Data"), QT_TR_NOOP("Save Data"), "save_data"}, + {Core::ContentType::Extdata, QT_TR_NOOP("Extra Data"), QT_TR_NOOP("Extra Data"), "save_data"}, + {Core::ContentType::SystemArchive, QT_TR_NOOP("System Archive"), QT_TR_NOOP("System Archives"), "system_archive"}, + {Core::ContentType::Sysdata, QT_TR_NOOP("System Data"), QT_TR_NOOP("System Data"), "system_data"}, + {Core::ContentType::SystemTitle, QT_TR_NOOP("System Title"), QT_TR_NOOP("System Titles"), "hos"}, + {Core::ContentType::SystemApplet, QT_TR_NOOP("System Applet"), QT_TR_NOOP("System Applets"), "hos"}, + }}; +// clang-format on + +static QString GetContentName(const Core::ContentSpecifier& specifier) { + return specifier.name.empty() + ? QStringLiteral("0x%1").arg(specifier.id, 16, 16, QLatin1Char('0')) + : QString::fromStdString(specifier.name); +} + +template +static QString GetContentTypeName(Core::ContentType type) { + if constexpr (Plural) { + return QObject::tr(std::get<2>(ContentTypeMap.at(static_cast(type))), + "ImportDialog"); + } else { + return QObject::tr(std::get<1>(ContentTypeMap.at(static_cast(type))), + "ImportDialog"); + } +} + +static QPixmap GetContentTypeIcon(Core::ContentType type) { + return QIcon::fromTheme( + QString::fromUtf8(std::get<3>(ContentTypeMap.at(static_cast(type))))) + .pixmap(24); +} + +static QPixmap GetContentIcon(const Core::ContentSpecifier& specifier, + bool use_category_icon = false) { + if (specifier.icon.empty()) { + // Return a category icon, or a null icon + return use_category_icon ? GetContentTypeIcon(specifier.type) + : QIcon::fromTheme(QStringLiteral("unknown")).pixmap(24); + } + return QPixmap::fromImage(QImage(reinterpret_cast(specifier.icon.data()), 24, 24, + QImage::Format::Format_RGB16)); +} + +ImportDialog::ImportDialog(QWidget* parent, const Core::Config& config_) + : DPIAwareDialog(parent, 560, 320), ui(std::make_unique()), config(config_) { + + qRegisterMetaType("u64"); + qRegisterMetaType("std::size_t"); + qRegisterMetaType(); + + ui->setupUi(this); + setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint)); + + RelistContent(); + UpdateSizeDisplay(); + + ui->title_view_button->setChecked(true); + + ui->buttonBox->button(QDialogButtonBox::StandardButton::Reset)->setText(tr("Refresh")); + connect(ui->buttonBox, &QDialogButtonBox::clicked, [this](QAbstractButton* button) { + if (button == ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)) { + StartImporting(); + } else if (button == ui->buttonBox->button(QDialogButtonBox::StandardButton::Cancel)) { + reject(); + } else { + RelistContent(); + } + }); + + connect(ui->title_view_button, &QRadioButton::toggled, this, &ImportDialog::RepopulateContent); + connect(ui->advanced_button, &QPushButton::clicked, this, &ImportDialog::ShowAdvancedMenu); + + ui->main->sortByColumn(-1, Qt::AscendingOrder); // disable sorting by default + ui->main->header()->setStretchLastSection(false); + connect(ui->main, &QTreeWidget::customContextMenuRequested, this, &ImportDialog::OnContextMenu); +} + +ImportDialog::~ImportDialog() = default; + +void ImportDialog::SetContentSizes(int previous_width, int previous_height) { + const int current_width = width(); + if (previous_width == 0) { // first time + ui->main->setColumnWidth(0, current_width * 0.66); + ui->main->setColumnWidth(1, current_width * 0.145); + ui->main->setColumnWidth(2, current_width * 0.10); + } else { // proportionally update column widths + for (int i : {0, 1, 2}) { + ui->main->setColumnWidth(i, ui->main->columnWidth(i) * current_width / previous_width); + } + } +} + +void ImportDialog::RelistContent() { + auto* dialog = + new RateLimitedProgressDialog(tr("Loading Contents..."), tr("Cancel"), 0, 0, this); + dialog->setCancelButton(nullptr); + + using FutureWatcher = QFutureWatcher; + auto* future_watcher = new FutureWatcher(this); + connect(future_watcher, &FutureWatcher::finished, this, [this, dialog] { + dialog->hide(); + if (importer->IsGood()) { + 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( + [&importer = this->importer, &config = this->config, &contents = this->contents] { + if (!importer) { + importer = std::make_unique(config); + } + if (importer->IsGood()) { + contents = importer->ListContent(); + } + }); + future_watcher->setFuture(future); +} + +constexpr Qt::ItemDataRole SpecifierIndexRole = Qt::UserRole; + +/// Supports readable size display and sorting +class ContentListItem final : public QTreeWidgetItem { +public: + explicit ContentListItem(QString name, u64 content_size_, QString exists) + : QTreeWidgetItem{{std::move(name), ReadableByteSize(content_size_), std::move(exists)}}, + content_size(content_size_) {} + + explicit ContentListItem(QString name, u64 content_size_, QString exists, std::size_t idx) + : QTreeWidgetItem{{std::move(name), ReadableByteSize(content_size_), std::move(exists)}}, + content_size(content_size_) { + setData(0, SpecifierIndexRole, static_cast(idx)); + } + ~ContentListItem() override = default; + +private: + bool operator<(const QTreeWidgetItem& other_item) const override { + const auto* other = dynamic_cast(&other_item); + if (!other) { + return false; + } + + const int column = treeWidget()->sortColumn(); + if (column == 1) { // size + return content_size < other->content_size; + } else { + return text(column) < other->text(column); + } + } + + u64 content_size; +}; + +void ImportDialog::InsertTopLevelItem(QString text, QPixmap icon) { + auto* item = new QTreeWidgetItem{{text}}; + item->setIcon(0, QIcon(std::move(icon))); + + item->setFlags(item->flags() | Qt::ItemIsAutoTristate); + item->setCheckState(0, Qt::Unchecked); // required to give the item a checkbox + + ui->main->invisibleRootItem()->addChild(item); + item->setFirstColumnSpanned(true); +} + +void ImportDialog::InsertTopLevelItem(QString text, QPixmap icon, u64 total_size, QString exists) { + auto* item = new ContentListItem{std::move(text), total_size, std::move(exists)}; + item->setIcon(0, QIcon(std::move(icon))); + + item->setFlags(item->flags() | Qt::ItemIsAutoTristate); + item->setCheckState(0, Qt::Unchecked); // required to give the item a checkbox + + ui->main->invisibleRootItem()->addChild(item); +} + +// Content types that themselves form a 'Title' like entity. +constexpr std::array SpecialContentTypeList{{ + Core::ContentType::SystemArchive, + Core::ContentType::Sysdata, + Core::ContentType::SystemTitle, + Core::ContentType::SystemApplet, +}}; + +void ImportDialog::InsertSecondLevelItem(std::size_t row, const Core::ContentSpecifier& content, + std::size_t id, QString replace_name, + QPixmap replace_icon) { + const bool use_title_view = ui->title_view_button->isChecked(); + + QString name; + if (use_title_view) { + if (row == 0) { + name = QStringLiteral("%1 (%2)") + .arg(GetContentName(content)) + .arg(GetContentTypeName(content.type)); + } else if (row <= SpecialContentTypeList.size()) { + name = GetContentName(content); + } else { + name = GetContentTypeName(content.type); + } + } else { + name = GetContentName(content); + } + + if (!replace_name.isEmpty()) { + name = replace_name; + } + + auto* item = new ContentListItem{name, content.maximum_size, + content.already_exists ? tr("Yes") : tr("No"), id}; + + // Set icon + QPixmap icon; + if (replace_icon.isNull()) { + // Exclude system titles, they are a single group but have own icons. + if (use_title_view && content.type != Core::ContentType::SystemTitle && + content.type != Core::ContentType::SystemApplet) { + icon = GetContentTypeIcon(content.type); + } else { + // When not in title view, System Data and System Archive groups use category icons. + const bool use_category_icon = content.type == Core::ContentType::Sysdata || + content.type == Core::ContentType::SystemArchive; + icon = GetContentIcon(content, use_category_icon); + } + } else { + icon = replace_icon; + } + item->setIcon(0, QIcon(icon)); + + // Skip System Applets, but enable everything else by default. + if (!content.already_exists && content.type != Core::ContentType::SystemApplet) { + item->setCheckState(0, Qt::Checked); + total_selected_size += content.maximum_size; + } else { + item->setCheckState(0, Qt::Unchecked); + } + + ui->main->invisibleRootItem()->child(row)->addChild(item); +} + +void ImportDialog::OnItemChanged(QTreeWidgetItem* item, int column) { + // Only handle second level items (with checkboxes) + if (column != 0 || !item->parent()) { + return; + } + + const auto& specifier = SpecifierFromItem(item); + if (item->checkState(0) == Qt::Checked) { + if (!applet_warning_shown && !specifier.already_exists && + specifier.type == Core::ContentType::SystemApplet) { + + QMessageBox::warning( + this, tr("Warning"), + tr("You are trying to import System Applets.\nThese are known to cause problems " + "with certain games.\nOnly proceed if you understand what you are doing.")); + applet_warning_shown = true; + } + total_selected_size += specifier.maximum_size; + } else { + if (!system_warning_shown && !specifier.already_exists && + (specifier.type == Core::ContentType::SystemArchive || + specifier.type == Core::ContentType::Sysdata || + specifier.type == Core::ContentType::SystemTitle)) { + + QMessageBox::warning(this, tr("Warning"), + tr("You are de-selecting important files that may be necessary " + "for your imported games to run.\nIt is highly recommended to " + "import these contents if they do not exist yet.")); + system_warning_shown = true; + } + total_selected_size -= specifier.maximum_size; + } + UpdateSizeDisplay(); +} + +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; + ui->main->clear(); + ui->main->setSortingEnabled(false); + disconnect(ui->main, &QTreeWidget::itemChanged, this, &ImportDialog::OnItemChanged); + + struct TitleMapEntry { + QString name; + QPixmap icon; + std::vector contents; + }; + std::map title_map; + std::unordered_map extdata_id_map; // extdata ID -> title ID + for (const auto& content : contents) { + if (content.type == Core::ContentType::Application) { + title_map[content.id].name = GetContentName(content); + title_map[content.id].icon = GetContentIcon(content); + extdata_id_map.emplace(content.extdata_id, content.id); + } + } + for (const auto& content : contents) { + if (content.type == Core::ContentType::Extdata) { + if (extdata_id_map.count(content.id)) { + const u64 title_id = extdata_id_map.at(content.id); + title_map[title_id].contents.emplace_back(&content); + } + } else if (content.type == Core::ContentType::Application || + content.type == Core::ContentType::Update || + content.type == Core::ContentType::DLC || + content.type == Core::ContentType::Savegame) { + if (title_map.count(content.id)) { + title_map[content.id].contents.emplace_back(&content); + } + } + } + + const bool use_title_view = ui->title_view_button->isChecked(); + if (use_title_view) { + // Create 'Ungrouped' category. + InsertTopLevelItem(tr("Ungrouped"), QIcon::fromTheme(QStringLiteral("unknown")).pixmap(24)); + + // Create categories for special content types. + for (std::size_t i = 0; i < SpecialContentTypeList.size(); ++i) { + InsertTopLevelItem(GetContentTypeName(SpecialContentTypeList[i]), + GetContentTypeIcon(SpecialContentTypeList[i])); + } + + // Titles + std::unordered_map title_row_map; + for (auto& [id, entry] : title_map) { + // Process the title's contents + u64 total_size = 0; + bool has_exist = false, has_non_exist = false; + for (const auto* content : entry.contents) { + total_size += content->maximum_size; + if (content->already_exists) { + has_exist = true; + } else { + has_non_exist = true; + } + } + + QString exist_text; + if (!has_exist) { + exist_text = tr("No"); + } else if (!has_non_exist) { + exist_text = tr("Yes"); + } else { + exist_text = tr("Part"); + } + + InsertTopLevelItem(std::move(entry.name), std::move(entry.icon), total_size, + std::move(exist_text)); + title_row_map.emplace(id, ui->main->invisibleRootItem()->childCount() - 1); + } + + for (std::size_t i = 0; i < contents.size(); ++i) { + const auto& content = contents[i]; + + std::size_t row = 0; // 0 for ungrouped (default) + switch (content.type) { + case Core::ContentType::Application: + case Core::ContentType::Update: + case Core::ContentType::DLC: + case Core::ContentType::Savegame: { + // Fix the id + const auto real_id = content.id & 0xffffff00ffffffff; + row = title_row_map.count(real_id) ? title_row_map.at(real_id) : 0; + break; + } + case Core::ContentType::Extdata: { + if (extdata_id_map.count(content.id)) { + row = title_row_map.at(extdata_id_map.at(content.id)); + } else { + row = 0; // Ungrouped + } + break; + } + default: { + const std::size_t idx = std::find(SpecialContentTypeList.begin(), + SpecialContentTypeList.end(), content.type) - + SpecialContentTypeList.begin(); + ASSERT_MSG(idx < SpecialContentTypeList.size(), "Content Type not handled"); + row = idx + 1; + break; + } + } + + InsertSecondLevelItem(row, content, i); + } + } else { + for (const auto& [type, singular_name, plural_name, icon_name] : ContentTypeMap) { + InsertTopLevelItem(tr(plural_name), GetContentTypeIcon(type)); + } + + for (std::size_t i = 0; i < contents.size(); ++i) { + const auto& content = contents[i]; + + QString name{}; + QPixmap icon{}; + if (content.type == Core::ContentType::Savegame) { + if (title_map.count(content.id)) { + name = title_map.at(content.id).name; + icon = title_map.at(content.id).icon; + } + } else if (content.type == Core::ContentType::Extdata) { + if (extdata_id_map.count(content.id)) { + u64 title_id = extdata_id_map.at(content.id); + name = title_map.at(title_id).name; + icon = title_map.at(title_id).icon; + } + } + + InsertSecondLevelItem(static_cast(content.type), content, i, name, icon); + } + } + + ui->main->setSortingEnabled(true); + connect(ui->main, &QTreeWidget::itemChanged, this, &ImportDialog::OnItemChanged); + UpdateSizeDisplay(); +} + +void ImportDialog::UpdateSizeDisplay() { + QStorageInfo storage(QString::fromStdString(config.user_path)); + if (!storage.isValid() || !storage.isReady()) { + LOG_ERROR(Frontend, "Storage {} is not good", config.user_path); + QMessageBox::critical( + this, tr("Bad Storage"), + tr("An error occured while trying to get available space for the storage.")); + reject(); + return; + } + + ui->availableSpace->setText( + tr("Available Space: %1").arg(ReadableByteSize(storage.bytesAvailable()))); + ui->totalSize->setText(tr("Total Size: %1").arg(ReadableByteSize(total_selected_size))); + + ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok) + ->setEnabled(total_selected_size > 0 && + total_selected_size <= static_cast(storage.bytesAvailable())); +} + +std::vector ImportDialog::GetSelectedContentList() { + std::vector to_import; + for (int i = 0; i < ui->main->invisibleRootItem()->childCount(); ++i) { + const auto* item = ui->main->invisibleRootItem()->child(i); + for (int j = 0; j < item->childCount(); ++j) { + if (item->child(j)->checkState(0) == Qt::Checked) { + to_import.emplace_back( + contents[item->child(j)->data(0, SpecifierIndexRole).toInt()]); + } + } + } + + return to_import; +} + +Core::ContentSpecifier ImportDialog::SpecifierFromItem(QTreeWidgetItem* item) const { + return contents[item->data(0, SpecifierIndexRole).toInt()]; +} + +void ImportDialog::OnContextMenu(const QPoint& point) { + QTreeWidgetItem* item = ui->main->itemAt(point.x(), point.y()); + if (!item) { + return; + } + + const bool title_view = ui->title_view_button->isChecked(); + + QMenu context_menu(this); + if (item->parent()) { // Second level + const auto& specifier = SpecifierFromItem(item); + if (specifier.type == Core::ContentType::Application) { + context_menu.addAction(tr("Dump CXI file"), + [this, specifier] { StartDumpingCXISingle(specifier); }); + } + if (Core::IsTitle(specifier.type)) { + context_menu.addAction(tr("Build CIA..."), + [this, specifier] { StartBuildingCIASingle(specifier); }); + context_menu.addAction(tr("Show Title Info"), [this, specifier] { + TitleInfoDialog dialog(this, *importer, specifier); + dialog.exec(); + }); + } + } else { // Top level + if (!title_view) { + return; + } + + for (int i = 0; i < item->childCount(); ++i) { + const auto& specifier = SpecifierFromItem(item->child(i)); + if (specifier.type == Core::ContentType::Application) { + context_menu.addAction(tr("Dump Base CXI file"), + [this, specifier] { StartDumpingCXISingle(specifier); }); + context_menu.addAction(tr("Build Base CIA"), + [this, specifier] { StartBuildingCIASingle(specifier); }); + } else if (specifier.type == Core::ContentType::Update) { + context_menu.addAction(tr("Build Update CIA"), + [this, specifier] { StartBuildingCIASingle(specifier); }); + } else if (specifier.type == Core::ContentType::DLC) { + context_menu.addAction(tr("Build DLC CIA"), + [this, specifier] { StartBuildingCIASingle(specifier); }); + } + } + } + context_menu.exec(ui->main->viewport()->mapToGlobal(point)); +} + +class AdvancedMenu : public QMenu { +public: + explicit AdvancedMenu(QWidget* parent) : QMenu(parent) {} + +private: + void mousePressEvent(QMouseEvent* event) override { + auto* dialog = static_cast(parentWidget()); + // Block popup menu when clicking on the Advanced button to dismiss the menu. + // With out this, it will immediately bring up the menu again. + if (dialog->childAt(dialog->mapFromGlobal(event->globalPos())) == + dialog->ui->advanced_button) { + + dialog->block_advanced_menu = true; + } + + QMenu::mousePressEvent(event); + } +}; + +void ImportDialog::ShowAdvancedMenu() { + if (block_advanced_menu) { + block_advanced_menu = false; + return; + } + + AdvancedMenu menu(this); + menu.addAction(tr("Batch Dump CXI"), this, &ImportDialog::StartBatchDumpingCXI); + menu.addAction(tr("Batch Build CIA"), this, &ImportDialog::StartBatchBuildingCIA); + + menu.exec(ui->advanced_button->mapToGlobal(ui->advanced_button->rect().bottomLeft())); +} + +static QString FormatETA(int eta) { + if (eta < 0) { + return QStringLiteral(" "); + } + return QCoreApplication::translate("ImportDialog", "ETA %1m%2s") + .arg(eta / 60, 2, 10, QLatin1Char('0')) + .arg(eta % 60, 2, 10, QLatin1Char('0')); +} + +// Runs the job, opening a dialog to report is progress. +void ImportDialog::RunMultiJob(MultiJob* job, std::size_t total_count, u64 total_size) { + // Try to map total_size to int range + // This is equal to ceil(total_size / INT_MAX) + const u64 multiplier = + (total_size + std::numeric_limits::max() - 1) / std::numeric_limits::max(); + + auto* label = new QLabel(tr("Initializing...")); + label->setWordWrap(true); + label->setFixedWidth(600); + + auto* dialog = new RateLimitedProgressDialog(tr("Initializing..."), tr("Cancel"), 0, + static_cast(total_size / multiplier), this); + dialog->setLabel(label); + + connect(job, &MultiJob::NextContent, this, + [this, dialog, multiplier, total_count](std::size_t count, u64 total_imported_size, + const Core::ContentSpecifier& next_content, + int eta) { + if (dialog->wasCanceled()) { + return; + } + dialog->Update(static_cast(total_imported_size / multiplier), + tr("

    (%1/%2) %3 (%4)

     

    %5

    ") + .arg(count) + .arg(total_count) + .arg(GetContentName(next_content)) + .arg(GetContentTypeName(next_content.type)) + .arg(FormatETA(eta))); + current_content = next_content; + current_count = count; + }); + connect( + job, &MultiJob::ProgressUpdated, this, + [this, dialog, multiplier, total_count](u64 current_imported_size, u64 total_imported_size, + int eta) { + if (dialog->wasCanceled()) { + return; + } + dialog->Update( + static_cast(total_imported_size / multiplier), + tr("

    (%1/%2) %3 (%4)

    %5 / %6

    %7

    ") + .arg(current_count) + .arg(total_count) + .arg(GetContentName(current_content)) + .arg(GetContentTypeName(current_content.type)) + .arg(ReadableByteSize(current_imported_size)) + .arg(ReadableByteSize(current_content.maximum_size)) + .arg(FormatETA(eta))); + }); + connect(job, &MultiJob::Completed, this, [this, dialog, job] { + dialog->hide(); + + const auto failed_contents = job->GetFailedContents(); + if (failed_contents.empty()) { + QMessageBox::information(this, tr("threeSD"), tr("All contents done successfully.")); + } else { + QString list_content; + for (const auto& content : failed_contents) { + list_content.append(QStringLiteral("
  • %1 (%2)
  • ") + .arg(GetContentName(content)) + .arg(GetContentTypeName(content.type))); + } + QMessageBox::critical(this, tr("threeSD"), + tr("List of failed contents:
      %1
    ").arg(list_content)); + } + + RelistContent(); + }); + connect(dialog, &QProgressDialog::canceled, this, [this, job] { + // Add yet-another-ProgressDialog to indicate cancel progress + auto* cancel_dialog = + new RateLimitedProgressDialog(tr("Canceling..."), tr("Cancel"), 0, 0, this); + cancel_dialog->setCancelButton(nullptr); + connect(job, &MultiJob::Completed, cancel_dialog, &QProgressDialog::hide); + job->Cancel(); + }); + + job->start(); +} + +void ImportDialog::StartImporting() { + UpdateSizeDisplay(); + if (!ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)->isEnabled()) { + // Space is no longer enough + QMessageBox::warning(this, tr("Not Enough Space"), + tr("Your disk does not have enough space to hold imported data.")); + return; + } + + auto to_import = GetSelectedContentList(); + const std::size_t total_count = to_import.size(); + + auto* job = + new MultiJob(this, *importer, std::move(to_import), &Core::SDMCImporter::ImportContent, + &Core::SDMCImporter::AbortImporting); + + RunMultiJob(job, total_count, total_selected_size); +} + +// CXI dumping + +void ImportDialog::StartDumpingCXISingle(const Core::ContentSpecifier& specifier) { + const QString path = QFileDialog::getSaveFileName(this, tr("Dump CXI file"), last_dump_cxi_path, + tr("CTR Executable Image (*.cxi)")); + if (path.isEmpty()) { + return; + } + last_dump_cxi_path = QFileInfo(path).path(); + + auto* job = new SimpleJob( + this, + [this, specifier, path](const Common::ProgressCallback& callback) { + return importer->DumpCXI(specifier, path.toStdString(), callback); + }, + [this] { importer->AbortDumpCXI(); }); + job->StartWithProgressDialog(this); +} + +void ImportDialog::StartBatchDumpingCXI() { + auto to_import = GetSelectedContentList(); + if (to_import.empty()) { + QMessageBox::warning(this, tr("threeSD"), + tr("Please select the contents you would like to dump as CXIs.")); + return; + } + + const auto removed_iter = std::remove_if( + to_import.begin(), to_import.end(), [](const Core::ContentSpecifier& specifier) { + return specifier.type != Core::ContentType::Application; + }); + if (removed_iter == to_import.begin()) { // No Applications selected + QMessageBox::critical(this, tr("threeSD"), + tr("The contents selected are not supported.
    You can only dump " + "Applications as CXIs.")); + return; + } + if (removed_iter != to_import.end()) { // Some non-Applications selected + QMessageBox::warning(this, tr("threeSD"), + tr("Some contents selected are not supported and will be " + "ignored.
    Only Applications will be dumped as CXIs.")); + } + + to_import.erase(removed_iter, to_import.end()); + + QString path = + QFileDialog::getExistingDirectory(this, tr("Batch Dump CXI"), last_batch_dump_cxi_path); + if (path.isEmpty()) { + return; + } + last_batch_dump_cxi_path = path; + if (!path.endsWith(QChar::fromLatin1('/')) && !path.endsWith(QChar::fromLatin1('\\'))) { + path.append(QStringLiteral("/")); + } + + const auto total_count = to_import.size(); + const auto total_size = std::accumulate(to_import.begin(), to_import.end(), u64{0}, + [](u64 sum, const Core::ContentSpecifier& specifier) { + return sum + specifier.maximum_size; + }); + auto* job = new MultiJob( + this, *importer, std::move(to_import), + [path](Core::SDMCImporter& importer, const Core::ContentSpecifier& specifier, + const Common::ProgressCallback& callback) { + return importer.DumpCXI(specifier, path.toStdString(), callback, true); + }, + &Core::SDMCImporter::AbortDumpCXI); + RunMultiJob(job, total_count, total_size); +} + +// CIA building + +void ImportDialog::StartBuildingCIASingle(const Core::ContentSpecifier& specifier) { + CIABuildDialog dialog(this, + /*is_dir*/ false, + /*is_nand*/ Core::IsNandTitle(specifier.type), + /*enable_legit*/ importer->CanBuildLegitCIA(specifier), + last_build_cia_path); + if (dialog.exec() != QDialog::Accepted) { + return; + } + const auto& [path, type] = dialog.GetResults(); + last_build_cia_path = QFileInfo(path).path(); + + auto* job = new SimpleJob( + this, + [this, specifier, path = path, type = type](const Common::ProgressCallback& callback) { + return importer->BuildCIA(type, specifier, path.toStdString(), callback); + }, + [this] { importer->AbortBuildCIA(); }); + job->StartWithProgressDialog(this); +} + +void ImportDialog::StartBatchBuildingCIA() { + auto to_import = GetSelectedContentList(); + if (to_import.empty()) { + QMessageBox::warning(this, tr("threeSD"), + tr("Please select the contents you would like to build as CIAs.")); + return; + } + + const auto removed_iter = std::remove_if( + to_import.begin(), to_import.end(), + [](const Core::ContentSpecifier& specifier) { return !Core::IsTitle(specifier.type); }); + if (removed_iter == to_import.begin()) { // No Titles selected + QMessageBox::critical(this, tr("threeSD"), + tr("The contents selected are not supported.
    You can only build " + "CIAs from Applications, Updates, DLCs and System Titles.")); + return; + } + if (removed_iter != to_import.end()) { // Some non-Titles selected + QMessageBox::warning( + this, tr("threeSD"), + tr("Some contents selected are not supported and will be ignored.
    Only " + "Applications, Updates, DLCs and System Titles will be built as CIAs.")); + } + + to_import.erase(removed_iter, to_import.end()); + + const bool is_nand = std::all_of( + to_import.begin(), to_import.end(), + [](const Core::ContentSpecifier& specifier) { return Core::IsNandTitle(specifier.type); }); + const bool enable_legit = std::all_of(to_import.begin(), to_import.end(), + [this](const Core::ContentSpecifier& specifier) { + return importer->CanBuildLegitCIA(specifier); + }); + CIABuildDialog dialog(this, /*is_dir*/ true, is_nand, enable_legit, last_batch_build_cia_path); + if (dialog.exec() != QDialog::Accepted) { + return; + } + auto [path, type] = dialog.GetResults(); + last_batch_build_cia_path = path; + if (!path.endsWith(QChar::fromLatin1('/')) && !path.endsWith(QChar::fromLatin1('\\'))) { + path.append(QStringLiteral("/")); + } + + const auto total_count = to_import.size(); + const auto total_size = std::accumulate(to_import.begin(), to_import.end(), u64{0}, + [](u64 sum, const Core::ContentSpecifier& specifier) { + return sum + specifier.maximum_size; + }); + auto* job = new MultiJob( + this, *importer, std::move(to_import), + [path = path, type = type](Core::SDMCImporter& importer, + const Core::ContentSpecifier& specifier, + const Common::ProgressCallback& callback) { + return importer.BuildCIA(type, specifier, path.toStdString(), callback, true); + }, + &Core::SDMCImporter::AbortBuildCIA); + RunMultiJob(job, total_count, total_size); +} diff --git a/src/frontend/import_dialog.h b/src/frontend/import_dialog.h index b248c9b..d674dd1 100644 --- a/src/frontend/import_dialog.h +++ b/src/frontend/import_dialog.h @@ -1,87 +1,87 @@ -// Copyright 2019 threeSD Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include -#include -#include -#include -#include "core/file_sys/ncch_container.h" -#include "core/importer.h" -#include "helpers/dpi_aware_dialog.h" - -class AdvancedMenu; -class MultiJob; -class SimpleJob; -class QTreeWidgetItem; - -namespace Ui { -class ImportDialog; -} - -class ImportDialog final : public DPIAwareDialog { - Q_OBJECT - -public: - explicit ImportDialog(QWidget* parent, const Core::Config& config); - ~ImportDialog() override; - -private: - void SetContentSizes(int previous_width, int previous_height) override; - - void RelistContent(); - void RepopulateContent(); - void UpdateSizeDisplay(); - std::vector GetSelectedContentList(); - - void InsertTopLevelItem(QString text, QPixmap icon = {}); - void InsertTopLevelItem(QString text, QPixmap icon, u64 total_size, QString exists); - // When replace_name and replace_icon are present they are used instead of those in `content`. - void InsertSecondLevelItem(std::size_t row, const Core::ContentSpecifier& content, - std::size_t id, QString replace_name = {}, - QPixmap replace_icon = {}); - - Core::ContentSpecifier SpecifierFromItem(QTreeWidgetItem* item) const; - - void OnContextMenu(const QPoint& point); - void ShowAdvancedMenu(); - - void OnItemChanged(QTreeWidgetItem* item, int column); - - void RunMultiJob(MultiJob* job, std::size_t total_count, u64 total_size); - - void StartImporting(); - - void StartDumpingCXISingle(const Core::ContentSpecifier& content); - QString last_dump_cxi_path; // Used for recording last path in StartDumpingCXISingle - void StartBatchDumpingCXI(); - QString last_batch_dump_cxi_path; // Used for recording last path in StartBatchDumpingCXI - - void StartBuildingCIASingle(const Core::ContentSpecifier& content); - QString last_build_cia_path; // Used for recording last path in StartBuildingCIASingle - void StartBatchBuildingCIA(); - QString last_batch_build_cia_path; // Used for recording last path in StartBatchBuildingCIA - - std::unique_ptr ui; - - std::unique_ptr importer; - const Core::Config config; - - std::vector contents; - u64 total_selected_size = 0; - - // HACK: Block advanced menu trigger once. - bool block_advanced_menu = false; - friend class AdvancedMenu; - - // Whether the System Archive / System Data warning has been shown - bool system_warning_shown = false; - // Whether the Applets warning has been shown - bool applet_warning_shown = false; - - // TODO: Why this won't work as locals? - Core::ContentSpecifier current_content = {}; - std::size_t current_count = 0; -}; +// Copyright 2019 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include "core/file_sys/ncch_container.h" +#include "core/importer.h" +#include "helpers/dpi_aware_dialog.h" + +class AdvancedMenu; +class MultiJob; +class SimpleJob; +class QTreeWidgetItem; + +namespace Ui { +class ImportDialog; +} + +class ImportDialog final : public DPIAwareDialog { + Q_OBJECT + +public: + explicit ImportDialog(QWidget* parent, const Core::Config& config); + ~ImportDialog() override; + +private: + void SetContentSizes(int previous_width, int previous_height) override; + + void RelistContent(); + void RepopulateContent(); + void UpdateSizeDisplay(); + std::vector GetSelectedContentList(); + + void InsertTopLevelItem(QString text, QPixmap icon = {}); + void InsertTopLevelItem(QString text, QPixmap icon, u64 total_size, QString exists); + // When replace_name and replace_icon are present they are used instead of those in `content`. + void InsertSecondLevelItem(std::size_t row, const Core::ContentSpecifier& content, + std::size_t id, QString replace_name = {}, + QPixmap replace_icon = {}); + + Core::ContentSpecifier SpecifierFromItem(QTreeWidgetItem* item) const; + + void OnContextMenu(const QPoint& point); + void ShowAdvancedMenu(); + + void OnItemChanged(QTreeWidgetItem* item, int column); + + void RunMultiJob(MultiJob* job, std::size_t total_count, u64 total_size); + + void StartImporting(); + + void StartDumpingCXISingle(const Core::ContentSpecifier& content); + QString last_dump_cxi_path; // Used for recording last path in StartDumpingCXISingle + void StartBatchDumpingCXI(); + QString last_batch_dump_cxi_path; // Used for recording last path in StartBatchDumpingCXI + + void StartBuildingCIASingle(const Core::ContentSpecifier& content); + QString last_build_cia_path; // Used for recording last path in StartBuildingCIASingle + void StartBatchBuildingCIA(); + QString last_batch_build_cia_path; // Used for recording last path in StartBatchBuildingCIA + + std::unique_ptr ui; + + std::unique_ptr importer; + const Core::Config config; + + std::vector contents; + u64 total_selected_size = 0; + + // HACK: Block advanced menu trigger once. + bool block_advanced_menu = false; + friend class AdvancedMenu; + + // Whether the System Archive / System Data warning has been shown + bool system_warning_shown = false; + // Whether the Applets warning has been shown + bool applet_warning_shown = false; + + // TODO: Why this won't work as locals? + Core::ContentSpecifier current_content = {}; + std::size_t current_count = 0; +}; diff --git a/src/frontend/import_dialog.ui b/src/frontend/import_dialog.ui index 8c01588..91ef65c 100644 --- a/src/frontend/import_dialog.ui +++ b/src/frontend/import_dialog.ui @@ -1,99 +1,99 @@ - - - ImportDialog - - - Select Contents - - - - - - - - Advanced... - - - - - - - Qt::Horizontal - - - - - - - Title View - - - - - - - Group View - - - - - - - - - Qt::CustomContextMenu - - - - Name - - - - - Size - - - - - Exists - - - - - - - - - - Available Space: - - - - - - - Qt::Horizontal - - - - - - - Total Size: - - - - - - - - - QDialogButtonBox::Ok|QDialogButtonBox::Cancel|QDialogButtonBox::Reset - - - - - - - - + + + ImportDialog + + + Select Contents + + + + + + + + Advanced... + + + + + + + Qt::Horizontal + + + + + + + Title View + + + + + + + Group View + + + + + + + + + Qt::CustomContextMenu + + + + Name + + + + + Size + + + + + Exists + + + + + + + + + + Available Space: + + + + + + + Qt::Horizontal + + + + + + + Total Size: + + + + + + + + + QDialogButtonBox::Ok|QDialogButtonBox::Cancel|QDialogButtonBox::Reset + + + + + + + + diff --git a/src/frontend/main.cpp b/src/frontend/main.cpp index 4c87190..1f74aac 100644 --- a/src/frontend/main.cpp +++ b/src/frontend/main.cpp @@ -1,258 +1,258 @@ -// Copyright 2014 Citra Emulator Project / 2019 threeSD Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include -#include -#include -#include -#include -#include -#include -#include -#include "common/assert.h" -#include "common/file_util.h" -#include "frontend/import_dialog.h" -#include "frontend/main.h" -#include "frontend/utilities.h" -#include "ui_main.h" - -#ifdef __APPLE__ -#include -#include "common/common_paths.h" -#endif - -#ifdef QT_STATICPLUGIN -#include - -Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin) -#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) -Q_IMPORT_PLUGIN(QWindowsVistaStylePlugin) -#endif -#endif - -bool IsConfigGood(const Core::Config& config) { - return !config.sdmc_path.empty() && !config.user_path.empty() && - !config.movable_sed_path.empty() && !config.bootrom_path.empty(); -} - -MainDialog::MainDialog(QWidget* parent) - : DPIAwareDialog(parent, 640, 256), ui(std::make_unique()) { - - ui->setupUi(this); - setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint)); - - ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)->setEnabled(false); - ui->buttonBox->button(QDialogButtonBox::StandardButton::Reset)->setText(tr("Refresh")); - - LoadPresetConfig(); - - connect(ui->utilitiesButton, &QPushButton::clicked, [this] { - UtilitiesDialog dialog(this); - dialog.exec(); - }); - - connect(ui->buttonBox, &QDialogButtonBox::clicked, [this](QAbstractButton* button) { - if (button == ui->buttonBox->button(QDialogButtonBox::StandardButton::Reset)) { - LoadPresetConfig(); - } else if (button == ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)) { - LaunchImportDialog(); - } - }); - - QString destination_text{}; - const auto destination = FileUtil::GetUserPathType(); - if (destination == FileUtil::UserPathType::Normal) { -#ifdef __linux__ - destination_text = tr("Non-Flatpak Citra Install"); -#else - destination_text = tr("User-wide Citra Install"); -#endif - } else if (destination == FileUtil::UserPathType::Portable) { - destination_text = tr("Portable Citra Install"); - } else if (destination == FileUtil::UserPathType::Flatpak) { - destination_text = tr("Flatpak Citra Install"); - } else { - UNREACHABLE(); - } - ui->importDestination->setText(tr("Import Destination: %1").arg(destination_text)); - - connect(ui->main, &QTreeWidget::itemSelectionChanged, [this] { - ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok) - ->setEnabled(!ui->main->selectedItems().empty()); - }); - connect(ui->main, &QTreeWidget::itemDoubleClicked, [this] { - if (!ui->main->selectedItems().empty()) - LaunchImportDialog(); - }); - - ui->main->setIndentation(4); - - // Set up device watcher - auto* device_watcher = new QDeviceWatcher(this); - device_watcher->start(); - connect(device_watcher, &QDeviceWatcher::deviceAdded, this, &MainDialog::LoadPresetConfig); - connect(device_watcher, &QDeviceWatcher::deviceChanged, this, &MainDialog::LoadPresetConfig); - connect(device_watcher, &QDeviceWatcher::deviceRemoved, this, &MainDialog::LoadPresetConfig); -} - -MainDialog::~MainDialog() = default; - -void MainDialog::SetContentSizes(int previous_width, int previous_height) { - const int current_width = width(); - if (previous_width == 0) { // first time - ui->main->setColumnWidth(0, 0.3 * current_width); - ui->main->setColumnWidth(1, 0.4 * current_width); - } else { // proportionally update column widths - for (int i : {0, 1}) { - ui->main->setColumnWidth(i, ui->main->columnWidth(i) * current_width / previous_width); - } - } -} - -static const std::regex sdmc_path_regex{"(.+)([/\\\\])Nintendo 3DS/([0-9a-f]{32})/([0-9a-f]{32})/"}; - -void MainDialog::LoadPresetConfig() { - ui->main->clear(); - preset_config_list.clear(); - - for (const auto& storage : QStorageInfo::mountedVolumes()) { - if (!storage.isValid() || !storage.isReady()) { - continue; - } - - auto list = Core::LoadPresetConfig(storage.rootPath().toStdString()); - for (std::size_t i = 0; i < list.size(); ++i) { - preset_config_list.emplace_back(list[i]); - - QString path = storage.rootPath(); - if (path.endsWith(QLatin1Char{'/'}) || path.endsWith(QLatin1Char{'\\'})) { - path.remove(path.size() - 1, 1); - } - - // Get ID0 - QString id0 = tr("Unknown"); - std::smatch match; - if (std::regex_match(list[i].sdmc_path, match, sdmc_path_regex)) { - if (match.size() >= 5) { - id0 = QString::fromStdString(match[3].str()); - } - } - - // Get status - QString status = tr("Good"); - if (!IsConfigGood(list[i])) { - status = tr("No Configuration Found"); - } else if (list[i].version != Core::CurrentDumperVersion) { - status = tr("Version Dismatch"); - } else if (list[i].safe_mode_firm_path.empty() || - list[i].config_savegame_path.empty() || - list[i].system_archives_path.empty()) { - - status = tr("Missing System Files"); - } else if (list[i].seed_db_path.empty()) { - status = tr("Good, Missing Seeds"); - } - - auto* item = new QTreeWidgetItem{{path, id0, status}}; - ui->main->invisibleRootItem()->addChild(item); - } - } - - auto* item = new QTreeWidgetItem{{tr("Browse SD Card Root...")}}; - item->setFirstColumnSpanned(true); - ui->main->invisibleRootItem()->addChild(item); -} - -void MainDialog::LaunchImportDialog() { - auto* item = ui->main->currentItem(); - if (!item) { - return; - } - - Core::Config config; - const auto index = ui->main->invisibleRootItem()->indexOfChild(item); - if (index == ui->main->invisibleRootItem()->childCount() - 1) { - const QString path = QFileDialog::getExistingDirectory(this, tr("Select SD Card Root")); - if (path.isEmpty()) { - return; - } - - const auto& list = Core::LoadPresetConfig(path.toStdString()); - if (list.size() > 1) { - QMessageBox::information( - this, tr("threeSD"), - tr("You have more than one 3DS data on your SD Card.\nthreeSD will " - "select the first one for you.")); - } else if (list.empty() || !IsConfigGood(list[0])) { - QMessageBox::critical( - this, tr("Error"), - tr("Could not load configuration.
    Please check if you have followed the " - "guide correctly.")); - return; - } - - config = list[0]; - } else { - config = preset_config_list.at(index); - } - - // Check config integrity - if (!IsConfigGood(config)) { - QMessageBox::critical( - this, tr("Error"), - tr("Could not load configuration from this SD card. You need to prepare your SD card " - "before using threeSD.
    Please check if you have followed the " - "guide correctly.")); - return; - } - - if (config.version != Core::CurrentDumperVersion) { - QMessageBox::critical(this, tr("Version Dismatch"), - tr("You are using an unsupported version of threeSDumper.
    Please " - "ensure that you are using the most recent version of both " - "threeSD and threeSDumper and try again.")); - return; - } - - if (config.safe_mode_firm_path.empty() || config.config_savegame_path.empty() || - config.system_archives_path.empty()) { - QMessageBox::warning( - this, tr("Warning"), - tr("Certain system files are missing from your configuration.
    Some contents " - "may not be importable, or may not run.
    Please check if you have followed the guide " - "correctly.")); - } else if (config.seed_db_path.empty()) { - QMessageBox::warning(this, tr("Warning"), - tr("Seed database is missing from your configuration.
    Your system " - "likely does not have any seeds.
    However, if it does have any, " - "imported games using seed encryption may not work.")); - } - - ImportDialog dialog(this, config); - dialog.exec(); -} - -int main(int argc, char* argv[]) { - // Init settings params - QCoreApplication::setOrganizationName(QStringLiteral("zhaowenlan1779")); - QCoreApplication::setApplicationName(QStringLiteral("threeSD")); - -#ifdef __APPLE__ - std::string bin_path = FileUtil::GetBundleDirectory() + DIR_SEP + ".."; - chdir(bin_path.c_str()); -#endif - - QApplication app(argc, argv); - - QIcon::setThemeSearchPaths(QStringList(QStringLiteral(":/icons/default"))); - QIcon::setThemeName(QStringLiteral(":/icons/default")); - - MainDialog main_dialog; - main_dialog.show(); - - return app.exec(); -} +// Copyright 2014 Citra Emulator Project / 2019 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include +#include +#include +#include "common/assert.h" +#include "common/file_util.h" +#include "frontend/import_dialog.h" +#include "frontend/main.h" +#include "frontend/utilities.h" +#include "ui_main.h" + +#ifdef __APPLE__ +#include +#include "common/common_paths.h" +#endif + +#ifdef QT_STATICPLUGIN +#include + +Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin) +#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) +Q_IMPORT_PLUGIN(QWindowsVistaStylePlugin) +#endif +#endif + +bool IsConfigGood(const Core::Config& config) { + return !config.sdmc_path.empty() && !config.user_path.empty() && + !config.movable_sed_path.empty() && !config.bootrom_path.empty(); +} + +MainDialog::MainDialog(QWidget* parent) + : DPIAwareDialog(parent, 640, 256), ui(std::make_unique()) { + + ui->setupUi(this); + setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint)); + + ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)->setEnabled(false); + ui->buttonBox->button(QDialogButtonBox::StandardButton::Reset)->setText(tr("Refresh")); + + LoadPresetConfig(); + + connect(ui->utilitiesButton, &QPushButton::clicked, [this] { + UtilitiesDialog dialog(this); + dialog.exec(); + }); + + connect(ui->buttonBox, &QDialogButtonBox::clicked, [this](QAbstractButton* button) { + if (button == ui->buttonBox->button(QDialogButtonBox::StandardButton::Reset)) { + LoadPresetConfig(); + } else if (button == ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)) { + LaunchImportDialog(); + } + }); + + QString destination_text{}; + const auto destination = FileUtil::GetUserPathType(); + if (destination == FileUtil::UserPathType::Normal) { +#ifdef __linux__ + destination_text = tr("Non-Flatpak Citra Install"); +#else + destination_text = tr("User-wide Citra Install"); +#endif + } else if (destination == FileUtil::UserPathType::Portable) { + destination_text = tr("Portable Citra Install"); + } else if (destination == FileUtil::UserPathType::Flatpak) { + destination_text = tr("Flatpak Citra Install"); + } else { + UNREACHABLE(); + } + ui->importDestination->setText(tr("Import Destination: %1").arg(destination_text)); + + connect(ui->main, &QTreeWidget::itemSelectionChanged, [this] { + ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok) + ->setEnabled(!ui->main->selectedItems().empty()); + }); + connect(ui->main, &QTreeWidget::itemDoubleClicked, [this] { + if (!ui->main->selectedItems().empty()) + LaunchImportDialog(); + }); + + ui->main->setIndentation(4); + + // Set up device watcher + auto* device_watcher = new QDeviceWatcher(this); + device_watcher->start(); + connect(device_watcher, &QDeviceWatcher::deviceAdded, this, &MainDialog::LoadPresetConfig); + connect(device_watcher, &QDeviceWatcher::deviceChanged, this, &MainDialog::LoadPresetConfig); + connect(device_watcher, &QDeviceWatcher::deviceRemoved, this, &MainDialog::LoadPresetConfig); +} + +MainDialog::~MainDialog() = default; + +void MainDialog::SetContentSizes(int previous_width, int previous_height) { + const int current_width = width(); + if (previous_width == 0) { // first time + ui->main->setColumnWidth(0, 0.3 * current_width); + ui->main->setColumnWidth(1, 0.4 * current_width); + } else { // proportionally update column widths + for (int i : {0, 1}) { + ui->main->setColumnWidth(i, ui->main->columnWidth(i) * current_width / previous_width); + } + } +} + +static const std::regex sdmc_path_regex{"(.+)([/\\\\])Nintendo 3DS/([0-9a-f]{32})/([0-9a-f]{32})/"}; + +void MainDialog::LoadPresetConfig() { + ui->main->clear(); + preset_config_list.clear(); + + for (const auto& storage : QStorageInfo::mountedVolumes()) { + if (!storage.isValid() || !storage.isReady()) { + continue; + } + + auto list = Core::LoadPresetConfig(storage.rootPath().toStdString()); + for (std::size_t i = 0; i < list.size(); ++i) { + preset_config_list.emplace_back(list[i]); + + QString path = storage.rootPath(); + if (path.endsWith(QLatin1Char{'/'}) || path.endsWith(QLatin1Char{'\\'})) { + path.remove(path.size() - 1, 1); + } + + // Get ID0 + QString id0 = tr("Unknown"); + std::smatch match; + if (std::regex_match(list[i].sdmc_path, match, sdmc_path_regex)) { + if (match.size() >= 5) { + id0 = QString::fromStdString(match[3].str()); + } + } + + // Get status + QString status = tr("Good"); + if (!IsConfigGood(list[i])) { + status = tr("No Configuration Found"); + } else if (list[i].version != Core::CurrentDumperVersion) { + status = tr("Version Dismatch"); + } else if (list[i].safe_mode_firm_path.empty() || + list[i].config_savegame_path.empty() || + list[i].system_archives_path.empty()) { + + status = tr("Missing System Files"); + } else if (list[i].seed_db_path.empty()) { + status = tr("Good, Missing Seeds"); + } + + auto* item = new QTreeWidgetItem{{path, id0, status}}; + ui->main->invisibleRootItem()->addChild(item); + } + } + + auto* item = new QTreeWidgetItem{{tr("Browse SD Card Root...")}}; + item->setFirstColumnSpanned(true); + ui->main->invisibleRootItem()->addChild(item); +} + +void MainDialog::LaunchImportDialog() { + auto* item = ui->main->currentItem(); + if (!item) { + return; + } + + Core::Config config; + const auto index = ui->main->invisibleRootItem()->indexOfChild(item); + if (index == ui->main->invisibleRootItem()->childCount() - 1) { + const QString path = QFileDialog::getExistingDirectory(this, tr("Select SD Card Root")); + if (path.isEmpty()) { + return; + } + + const auto& list = Core::LoadPresetConfig(path.toStdString()); + if (list.size() > 1) { + QMessageBox::information( + this, tr("threeSD"), + tr("You have more than one 3DS data on your SD Card.\nthreeSD will " + "select the first one for you.")); + } else if (list.empty() || !IsConfigGood(list[0])) { + QMessageBox::critical( + this, tr("Error"), + tr("Could not load configuration.
    Please check if you have followed the " + "guide correctly.")); + return; + } + + config = list[0]; + } else { + config = preset_config_list.at(index); + } + + // Check config integrity + if (!IsConfigGood(config)) { + QMessageBox::critical( + this, tr("Error"), + tr("Could not load configuration from this SD card. You need to prepare your SD card " + "before using threeSD.
    Please check if you have followed the " + "guide correctly.")); + return; + } + + if (config.version != Core::CurrentDumperVersion) { + QMessageBox::critical(this, tr("Version Dismatch"), + tr("You are using an unsupported version of threeSDumper.
    Please " + "ensure that you are using the most recent version of both " + "threeSD and threeSDumper and try again.")); + return; + } + + if (config.safe_mode_firm_path.empty() || config.config_savegame_path.empty() || + config.system_archives_path.empty()) { + QMessageBox::warning( + this, tr("Warning"), + tr("Certain system files are missing from your configuration.
    Some contents " + "may not be importable, or may not run.
    Please check if you have followed the guide " + "correctly.")); + } else if (config.seed_db_path.empty()) { + QMessageBox::warning(this, tr("Warning"), + tr("Seed database is missing from your configuration.
    Your system " + "likely does not have any seeds.
    However, if it does have any, " + "imported games using seed encryption may not work.")); + } + + ImportDialog dialog(this, config); + dialog.exec(); +} + +int main(int argc, char* argv[]) { + // Init settings params + QCoreApplication::setOrganizationName(QStringLiteral("zhaowenlan1779")); + QCoreApplication::setApplicationName(QStringLiteral("threeSD")); + +#ifdef __APPLE__ + std::string bin_path = FileUtil::GetBundleDirectory() + DIR_SEP + ".."; + chdir(bin_path.c_str()); +#endif + + QApplication app(argc, argv); + + QIcon::setThemeSearchPaths(QStringList(QStringLiteral(":/icons/default"))); + QIcon::setThemeName(QStringLiteral(":/icons/default")); + + MainDialog main_dialog; + main_dialog.show(); + + return app.exec(); +} diff --git a/src/frontend/main.h b/src/frontend/main.h index 4199b3e..e0a145a 100644 --- a/src/frontend/main.h +++ b/src/frontend/main.h @@ -1,30 +1,30 @@ -// Copyright 2019 threeSD Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include -#include "core/importer.h" -#include "frontend/helpers/dpi_aware_dialog.h" - -namespace Ui { -class MainDialog; -} - -class MainDialog final : public DPIAwareDialog { - Q_OBJECT - -public: - explicit MainDialog(QWidget* parent = nullptr); - ~MainDialog() override; - -private: - void SetContentSizes(int previous_width, int previous_height) override; - - void LoadPresetConfig(); - void LaunchImportDialog(); - - std::vector preset_config_list; - std::unique_ptr ui; -}; +// Copyright 2019 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include "core/importer.h" +#include "frontend/helpers/dpi_aware_dialog.h" + +namespace Ui { +class MainDialog; +} + +class MainDialog final : public DPIAwareDialog { + Q_OBJECT + +public: + explicit MainDialog(QWidget* parent = nullptr); + ~MainDialog() override; + +private: + void SetContentSizes(int previous_width, int previous_height) override; + + void LoadPresetConfig(); + void LaunchImportDialog(); + + std::vector preset_config_list; + std::unique_ptr ui; +}; diff --git a/src/frontend/main.ui b/src/frontend/main.ui index 40cc3c4..7b2642c 100644 --- a/src/frontend/main.ui +++ b/src/frontend/main.ui @@ -1,78 +1,78 @@ - - - MainDialog - - - threeSD - - - - - - - - Select your SD card root, or manually browse when not detected. - - - - - - - Qt::Horizontal - - - - - - - Utilities... - - - - - - - - - - Path - - - - - ID - - - - - Status - - - - - - - - - - - - - Qt::Horizontal - - - - - - - - - QDialogButtonBox::Ok|QDialogButtonBox::Reset - - - - - - - - + + + MainDialog + + + threeSD + + + + + + + + Select your SD card root, or manually browse when not detected. + + + + + + + Qt::Horizontal + + + + + + + Utilities... + + + + + + + + + + Path + + + + + ID + + + + + Status + + + + + + + + + + + + + Qt::Horizontal + + + + + + + + + QDialogButtonBox::Ok|QDialogButtonBox::Reset + + + + + + + + diff --git a/src/frontend/select_files_dialog.cpp b/src/frontend/select_files_dialog.cpp index 2be182d..1d3197b 100644 --- a/src/frontend/select_files_dialog.cpp +++ b/src/frontend/select_files_dialog.cpp @@ -1,55 +1,55 @@ -// Copyright 2020 threeSD Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include -#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_) - : DPIAwareDialog(parent, 480, 96), ui(std::make_unique()), - source_is_dir(source_is_dir_), destination_is_dir(destination_is_dir_) { - - ui->setupUi(this); - setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint)); - - 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()}; -} +// Copyright 2020 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#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_) + : DPIAwareDialog(parent, 480, 96), ui(std::make_unique()), + source_is_dir(source_is_dir_), destination_is_dir(destination_is_dir_) { + + ui->setupUi(this); + setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint)); + + 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 index 403a44e..5dcfc3d 100644 --- a/src/frontend/select_files_dialog.h +++ b/src/frontend/select_files_dialog.h @@ -1,27 +1,27 @@ -// Copyright 2020 threeSD Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include -#include "frontend/helpers/dpi_aware_dialog.h" - -namespace Ui { -class SelectFilesDialog; -} - -class SelectFilesDialog : public DPIAwareDialog { - 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 -}; +// Copyright 2020 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include "frontend/helpers/dpi_aware_dialog.h" + +namespace Ui { +class SelectFilesDialog; +} + +class SelectFilesDialog : public DPIAwareDialog { + 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 index 467f20d..f27e88a 100644 --- a/src/frontend/select_files_dialog.ui +++ b/src/frontend/select_files_dialog.ui @@ -1,68 +1,68 @@ - - - SelectFilesDialog - - - Select Files - - - - - - - - Source: - - - - - - - - - - - 0 - 0 - - - - ... - - - - - - - Destination: - - - - - - - - - - - 0 - 0 - - - - ... - - - - - - - - - QDialogButtonBox::Ok|QDialogButtonBox::Cancel - - - - - - + + + SelectFilesDialog + + + Select Files + + + + + + + + Source: + + + + + + + + + + + 0 + 0 + + + + ... + + + + + + + Destination: + + + + + + + + + + + 0 + 0 + + + + ... + + + + + + + + + QDialogButtonBox::Ok|QDialogButtonBox::Cancel + + + + + + diff --git a/src/frontend/title_info_dialog.cpp b/src/frontend/title_info_dialog.cpp index 4e85153..6a32684 100644 --- a/src/frontend/title_info_dialog.cpp +++ b/src/frontend/title_info_dialog.cpp @@ -1,241 +1,241 @@ -// Copyright 2021 threeSD Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include -#include -#include -#include -#include -#include -#include -#include "common/string_util.h" -#include "core/db/title_db.h" -#include "core/file_sys/ncch_container.h" -#include "core/file_sys/title_metadata.h" -#include "core/importer.h" -#include "frontend/helpers/simple_job.h" -#include "frontend/title_info_dialog.h" -#include "ui_title_info_dialog.h" - -TitleInfoDialog::TitleInfoDialog(QWidget* parent, Core::SDMCImporter& importer_, - Core::ContentSpecifier specifier_) - : DPIAwareDialog(parent, 500, 360), ui(std::make_unique()), - importer(importer_), specifier(std::move(specifier_)) { - - ui->setupUi(this); - setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint)); - - LoadInfo(); - - connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &TitleInfoDialog::accept); -} - -TitleInfoDialog::~TitleInfoDialog() = default; - -void TitleInfoDialog::LoadInfo() { - // Load TMD & boot NCCH - Core::TitleMetadata tmd; - Core::NCCHContainer ncch; - if (!importer.LoadTMD(specifier, tmd) || - !ncch.OpenFile(importer.OpenContent(specifier, tmd.GetBootContentID()))) { - - QMessageBox::warning(this, tr("threeSD"), tr("Could not load title information.")); - reject(); - return; - } - - // Load SMDH from boot NCCH - bool has_smdh = false; - std::vector smdh_buffer; - if (ncch.LoadSectionExeFS("icon", smdh_buffer) && smdh_buffer.size() == sizeof(Core::SMDH) && - Core::IsValidSMDH(smdh_buffer)) { - - has_smdh = true; - std::memcpy(&smdh, smdh_buffer.data(), smdh_buffer.size()); - } - - // Basic info - ui->versionLineEdit->setText(QString::fromStdString(tmd.GetTitleVersionString())); - LoadEncryption(ncch); - ui->titleIDLineEdit->setText(QStringLiteral("%1").arg(specifier.id, 16, 16, QLatin1Char{'0'})); - - // Icons - if (has_smdh) { - LoadIcons(); - } - - // Names - if (has_smdh) { - InitializeLanguageComboBox(); - } else { - ui->namesGroupBox->setVisible(false); - } - - // Checks - InitializeChecks(tmd); -} - -void TitleInfoDialog::LoadEncryption(Core::NCCHContainer& ncch) { - static const std::unordered_map EncryptionTypeMap{{ - {Core::EncryptionType::None, QT_TR_NOOP("None")}, - {Core::EncryptionType::FixedKey, QT_TR_NOOP("FixedKey")}, - {Core::EncryptionType::NCCHSecure1, QT_TR_NOOP("Secure1")}, - {Core::EncryptionType::NCCHSecure2, QT_TR_NOOP("Secure2")}, - {Core::EncryptionType::NCCHSecure3, QT_TR_NOOP("Secure3")}, - {Core::EncryptionType::NCCHSecure4, QT_TR_NOOP("Secure4")}, - }}; - - Core::EncryptionType encryption = Core::EncryptionType::None; - ncch.ReadEncryptionType(encryption); - - bool seed_crypto = false; - ncch.ReadSeedCrypto(seed_crypto); - - QString encryption_text = tr(EncryptionTypeMap.at(encryption)); - if (seed_crypto) { - encryption_text.append(tr(" (Seed)")); - } - ui->encryptionLineEdit->setText(encryption_text); -} - -void TitleInfoDialog::LoadIcons() { - ui->iconLargeLabel->setPixmap( - QPixmap::fromImage(QImage(reinterpret_cast(smdh.GetIcon(true).data()), 48, 48, - QImage::Format::Format_RGB16))); - - QAction* save_icon_large = new QAction(tr("Save Icon (Large)"), this); - ui->iconLargeLabel->addAction(save_icon_large); - connect(save_icon_large, &QAction::triggered, this, [this] { SaveIcon(true); }); - - ui->iconSmallLabel->setPixmap( - QPixmap::fromImage(QImage(reinterpret_cast(smdh.GetIcon(false).data()), 24, - 24, QImage::Format::Format_RGB16))); - - QAction* save_icon_small = new QAction(tr("Save Icon (Small)"), this); - ui->iconSmallLabel->addAction(save_icon_small); - connect(save_icon_small, &QAction::triggered, this, [this] { SaveIcon(false); }); -} - -void TitleInfoDialog::InitializeLanguageComboBox() { - if (!ui->namesGroupBox->isEnabled()) { // SMDH not available - return; - } - // Corresponds to the indices of the languages defined in SMDH - static constexpr std::array LanguageNames{{ - QT_TR_NOOP("Japanese"), - QT_TR_NOOP("English"), - QT_TR_NOOP("French"), - QT_TR_NOOP("German"), - QT_TR_NOOP("Italian"), - QT_TR_NOOP("Spanish"), - QT_TR_NOOP("Chinese (Simplified)"), - QT_TR_NOOP("Korean"), - QT_TR_NOOP("Dutch"), - QT_TR_NOOP("Portuguese"), - QT_TR_NOOP("Russian"), - QT_TR_NOOP("Chinese (Traditional)"), - }}; - for (std::size_t i = 0; i < LanguageNames.size(); ++i) { - const auto& title = smdh.titles.at(i); - if (Common::UTF16BufferToUTF8(title.short_title).empty() && - Common::UTF16BufferToUTF8(title.long_title).empty() && - Common::UTF16BufferToUTF8(title.publisher).empty()) { - // All empty, ignore this language - continue; - } - - ui->languageComboBox->addItem(tr(LanguageNames.at(i)), static_cast(i)); - if (i == 1) { // English - ui->languageComboBox->setCurrentIndex(ui->languageComboBox->count() - 1); - } - } - connect(ui->languageComboBox, qOverload(&QComboBox::currentIndexChanged), this, - &TitleInfoDialog::UpdateNames); - UpdateNames(); -} - -void TitleInfoDialog::SaveIcon(bool large) { - const auto types = QImageWriter::supportedImageFormats(); - QStringList filters; - for (const auto& type : types) { - const QString extension = QString::fromUtf8(type); - filters << QStringLiteral("%1 Image (*.%2)").arg(extension.toUpper(), extension); - } - - static QString last_path; - const QString path = QFileDialog::getSaveFileName(this, tr("Save Icon"), last_path, - filters.join(QStringLiteral(";;"))); - if (path.isEmpty()) { - return; - } - last_path = QFileInfo(path).path(); - -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) - const auto& pixmap = large ? ui->iconLargeLabel->pixmap(Qt::ReturnByValue) - : ui->iconSmallLabel->pixmap(Qt::ReturnByValue); -#else - const auto& pixmap = large ? *ui->iconLargeLabel->pixmap() : *ui->iconSmallLabel->pixmap(); -#endif - if (!pixmap.save(path)) { - QMessageBox::warning(this, tr("threeSD"), tr("Could not save icon.")); - } -} - -void TitleInfoDialog::UpdateNames() { - const auto& title = smdh.titles.at(ui->languageComboBox->currentData().toInt()); - ui->shortTitleLineEdit->setText( - QString::fromStdString(Common::UTF16BufferToUTF8(title.short_title))); - ui->longTitleLineEdit->setText( - QString::fromStdString(Common::UTF16BufferToUTF8(title.long_title))); - ui->publisherLineEdit->setText( - QString::fromStdString(Common::UTF16BufferToUTF8(title.publisher))); -} - -void TitleInfoDialog::InitializeChecks(Core::TitleMetadata& tmd) { - const bool tmd_legit = tmd.ValidateSignature() && tmd.VerifyHashes(); - if (tmd_legit) { - ui->tmdCheckLabel->setText(tr("Legit")); - } else { - ui->tmdCheckLabel->setText(tr("Illegit")); - } - - if (const auto& ticket_db = importer.GetTicketDB(); - ticket_db && ticket_db->tickets.count(specifier.id)) { - - const bool ticket_legit = ticket_db->tickets.at(specifier.id).ValidateSignature(); - if (ticket_legit) { - ui->ticketCheckLabel->setText(tr("Legit")); - } else { - ui->ticketCheckLabel->setText(tr("Illegit")); - } - } else { - ui->ticketCheckLabel->setText(tr("Missing")); - } - connect(ui->contentsCheckButton, &QPushButton::clicked, this, - &TitleInfoDialog::ExecuteContentsCheck); -} - -void TitleInfoDialog::ExecuteContentsCheck() { - auto* job = new SimpleJob( - this, - [this](const Common::ProgressCallback& callback) { - contents_check_result = importer.CheckTitleContents(specifier, callback); - return true; - }, - [this] { importer.AbortImporting(); }); - connect(job, &SimpleJob::Completed, this, [this](bool canceled) { - if (canceled) { - return; - } - - ui->contentsCheckButton->setVisible(false); - ui->contentsCheckLabel->setVisible(true); - if (contents_check_result) { - ui->contentsCheckLabel->setText(tr("OK")); - } else { - ui->contentsCheckLabel->setText(tr("Failed")); - } - }); - job->StartWithProgressDialog(this); -} +// Copyright 2021 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include +#include +#include "common/string_util.h" +#include "core/db/title_db.h" +#include "core/file_sys/ncch_container.h" +#include "core/file_sys/title_metadata.h" +#include "core/importer.h" +#include "frontend/helpers/simple_job.h" +#include "frontend/title_info_dialog.h" +#include "ui_title_info_dialog.h" + +TitleInfoDialog::TitleInfoDialog(QWidget* parent, Core::SDMCImporter& importer_, + Core::ContentSpecifier specifier_) + : DPIAwareDialog(parent, 500, 360), ui(std::make_unique()), + importer(importer_), specifier(std::move(specifier_)) { + + ui->setupUi(this); + setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint)); + + LoadInfo(); + + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &TitleInfoDialog::accept); +} + +TitleInfoDialog::~TitleInfoDialog() = default; + +void TitleInfoDialog::LoadInfo() { + // Load TMD & boot NCCH + Core::TitleMetadata tmd; + Core::NCCHContainer ncch; + if (!importer.LoadTMD(specifier, tmd) || + !ncch.OpenFile(importer.OpenContent(specifier, tmd.GetBootContentID()))) { + + QMessageBox::warning(this, tr("threeSD"), tr("Could not load title information.")); + reject(); + return; + } + + // Load SMDH from boot NCCH + bool has_smdh = false; + std::vector smdh_buffer; + if (ncch.LoadSectionExeFS("icon", smdh_buffer) && smdh_buffer.size() == sizeof(Core::SMDH) && + Core::IsValidSMDH(smdh_buffer)) { + + has_smdh = true; + std::memcpy(&smdh, smdh_buffer.data(), smdh_buffer.size()); + } + + // Basic info + ui->versionLineEdit->setText(QString::fromStdString(tmd.GetTitleVersionString())); + LoadEncryption(ncch); + ui->titleIDLineEdit->setText(QStringLiteral("%1").arg(specifier.id, 16, 16, QLatin1Char{'0'})); + + // Icons + if (has_smdh) { + LoadIcons(); + } + + // Names + if (has_smdh) { + InitializeLanguageComboBox(); + } else { + ui->namesGroupBox->setVisible(false); + } + + // Checks + InitializeChecks(tmd); +} + +void TitleInfoDialog::LoadEncryption(Core::NCCHContainer& ncch) { + static const std::unordered_map EncryptionTypeMap{{ + {Core::EncryptionType::None, QT_TR_NOOP("None")}, + {Core::EncryptionType::FixedKey, QT_TR_NOOP("FixedKey")}, + {Core::EncryptionType::NCCHSecure1, QT_TR_NOOP("Secure1")}, + {Core::EncryptionType::NCCHSecure2, QT_TR_NOOP("Secure2")}, + {Core::EncryptionType::NCCHSecure3, QT_TR_NOOP("Secure3")}, + {Core::EncryptionType::NCCHSecure4, QT_TR_NOOP("Secure4")}, + }}; + + Core::EncryptionType encryption = Core::EncryptionType::None; + ncch.ReadEncryptionType(encryption); + + bool seed_crypto = false; + ncch.ReadSeedCrypto(seed_crypto); + + QString encryption_text = tr(EncryptionTypeMap.at(encryption)); + if (seed_crypto) { + encryption_text.append(tr(" (Seed)")); + } + ui->encryptionLineEdit->setText(encryption_text); +} + +void TitleInfoDialog::LoadIcons() { + ui->iconLargeLabel->setPixmap( + QPixmap::fromImage(QImage(reinterpret_cast(smdh.GetIcon(true).data()), 48, 48, + QImage::Format::Format_RGB16))); + + QAction* save_icon_large = new QAction(tr("Save Icon (Large)"), this); + ui->iconLargeLabel->addAction(save_icon_large); + connect(save_icon_large, &QAction::triggered, this, [this] { SaveIcon(true); }); + + ui->iconSmallLabel->setPixmap( + QPixmap::fromImage(QImage(reinterpret_cast(smdh.GetIcon(false).data()), 24, + 24, QImage::Format::Format_RGB16))); + + QAction* save_icon_small = new QAction(tr("Save Icon (Small)"), this); + ui->iconSmallLabel->addAction(save_icon_small); + connect(save_icon_small, &QAction::triggered, this, [this] { SaveIcon(false); }); +} + +void TitleInfoDialog::InitializeLanguageComboBox() { + if (!ui->namesGroupBox->isEnabled()) { // SMDH not available + return; + } + // Corresponds to the indices of the languages defined in SMDH + static constexpr std::array LanguageNames{{ + QT_TR_NOOP("Japanese"), + QT_TR_NOOP("English"), + QT_TR_NOOP("French"), + QT_TR_NOOP("German"), + QT_TR_NOOP("Italian"), + QT_TR_NOOP("Spanish"), + QT_TR_NOOP("Chinese (Simplified)"), + QT_TR_NOOP("Korean"), + QT_TR_NOOP("Dutch"), + QT_TR_NOOP("Portuguese"), + QT_TR_NOOP("Russian"), + QT_TR_NOOP("Chinese (Traditional)"), + }}; + for (std::size_t i = 0; i < LanguageNames.size(); ++i) { + const auto& title = smdh.titles.at(i); + if (Common::UTF16BufferToUTF8(title.short_title).empty() && + Common::UTF16BufferToUTF8(title.long_title).empty() && + Common::UTF16BufferToUTF8(title.publisher).empty()) { + // All empty, ignore this language + continue; + } + + ui->languageComboBox->addItem(tr(LanguageNames.at(i)), static_cast(i)); + if (i == 1) { // English + ui->languageComboBox->setCurrentIndex(ui->languageComboBox->count() - 1); + } + } + connect(ui->languageComboBox, qOverload(&QComboBox::currentIndexChanged), this, + &TitleInfoDialog::UpdateNames); + UpdateNames(); +} + +void TitleInfoDialog::SaveIcon(bool large) { + const auto types = QImageWriter::supportedImageFormats(); + QStringList filters; + for (const auto& type : types) { + const QString extension = QString::fromUtf8(type); + filters << QStringLiteral("%1 Image (*.%2)").arg(extension.toUpper(), extension); + } + + static QString last_path; + const QString path = QFileDialog::getSaveFileName(this, tr("Save Icon"), last_path, + filters.join(QStringLiteral(";;"))); + if (path.isEmpty()) { + return; + } + last_path = QFileInfo(path).path(); + +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + const auto& pixmap = large ? ui->iconLargeLabel->pixmap(Qt::ReturnByValue) + : ui->iconSmallLabel->pixmap(Qt::ReturnByValue); +#else + const auto& pixmap = large ? *ui->iconLargeLabel->pixmap() : *ui->iconSmallLabel->pixmap(); +#endif + if (!pixmap.save(path)) { + QMessageBox::warning(this, tr("threeSD"), tr("Could not save icon.")); + } +} + +void TitleInfoDialog::UpdateNames() { + const auto& title = smdh.titles.at(ui->languageComboBox->currentData().toInt()); + ui->shortTitleLineEdit->setText( + QString::fromStdString(Common::UTF16BufferToUTF8(title.short_title))); + ui->longTitleLineEdit->setText( + QString::fromStdString(Common::UTF16BufferToUTF8(title.long_title))); + ui->publisherLineEdit->setText( + QString::fromStdString(Common::UTF16BufferToUTF8(title.publisher))); +} + +void TitleInfoDialog::InitializeChecks(Core::TitleMetadata& tmd) { + const bool tmd_legit = tmd.ValidateSignature() && tmd.VerifyHashes(); + if (tmd_legit) { + ui->tmdCheckLabel->setText(tr("Legit")); + } else { + ui->tmdCheckLabel->setText(tr("Illegit")); + } + + if (const auto& ticket_db = importer.GetTicketDB(); + ticket_db && ticket_db->tickets.count(specifier.id)) { + + const bool ticket_legit = ticket_db->tickets.at(specifier.id).ValidateSignature(); + if (ticket_legit) { + ui->ticketCheckLabel->setText(tr("Legit")); + } else { + ui->ticketCheckLabel->setText(tr("Illegit")); + } + } else { + ui->ticketCheckLabel->setText(tr("Missing")); + } + connect(ui->contentsCheckButton, &QPushButton::clicked, this, + &TitleInfoDialog::ExecuteContentsCheck); +} + +void TitleInfoDialog::ExecuteContentsCheck() { + auto* job = new SimpleJob( + this, + [this](const Common::ProgressCallback& callback) { + contents_check_result = importer.CheckTitleContents(specifier, callback); + return true; + }, + [this] { importer.AbortImporting(); }); + connect(job, &SimpleJob::Completed, this, [this](bool canceled) { + if (canceled) { + return; + } + + ui->contentsCheckButton->setVisible(false); + ui->contentsCheckLabel->setVisible(true); + if (contents_check_result) { + ui->contentsCheckLabel->setText(tr("OK")); + } else { + ui->contentsCheckLabel->setText(tr("Failed")); + } + }); + job->StartWithProgressDialog(this); +} diff --git a/src/frontend/title_info_dialog.h b/src/frontend/title_info_dialog.h index e4b18a9..d14f9c1 100644 --- a/src/frontend/title_info_dialog.h +++ b/src/frontend/title_info_dialog.h @@ -1,47 +1,47 @@ -// Copyright 2021 threeSD Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include -#include "core/file_sys/smdh.h" -#include "frontend/helpers/dpi_aware_dialog.h" - -namespace Core { -struct Config; -struct ContentSpecifier; -class NCCHContainer; -class SDMCImporter; -class TitleMetadata; -} // namespace Core - -namespace Ui { -class TitleInfoDialog; -} - -class TitleInfoDialog : public DPIAwareDialog { - Q_OBJECT - -public: - explicit TitleInfoDialog(QWidget* parent, Core::SDMCImporter& importer, - Core::ContentSpecifier specifier); - ~TitleInfoDialog(); - -private: - void LoadInfo(); - - void LoadEncryption(Core::NCCHContainer& ncch); - void LoadIcons(); - void InitializeLanguageComboBox(); - void InitializeChecks(Core::TitleMetadata& tmd); - - void SaveIcon(bool large); - void UpdateNames(); - void ExecuteContentsCheck(); - - std::unique_ptr ui; - - Core::SDMCImporter& importer; - const Core::ContentSpecifier specifier; - Core::SMDH smdh{}; - bool contents_check_result = false; -}; +// Copyright 2021 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include "core/file_sys/smdh.h" +#include "frontend/helpers/dpi_aware_dialog.h" + +namespace Core { +struct Config; +struct ContentSpecifier; +class NCCHContainer; +class SDMCImporter; +class TitleMetadata; +} // namespace Core + +namespace Ui { +class TitleInfoDialog; +} + +class TitleInfoDialog : public DPIAwareDialog { + Q_OBJECT + +public: + explicit TitleInfoDialog(QWidget* parent, Core::SDMCImporter& importer, + Core::ContentSpecifier specifier); + ~TitleInfoDialog(); + +private: + void LoadInfo(); + + void LoadEncryption(Core::NCCHContainer& ncch); + void LoadIcons(); + void InitializeLanguageComboBox(); + void InitializeChecks(Core::TitleMetadata& tmd); + + void SaveIcon(bool large); + void UpdateNames(); + void ExecuteContentsCheck(); + + std::unique_ptr ui; + + Core::SDMCImporter& importer; + const Core::ContentSpecifier specifier; + Core::SMDH smdh{}; + bool contents_check_result = false; +}; diff --git a/src/frontend/title_info_dialog.ui b/src/frontend/title_info_dialog.ui index 79525ea..75738d7 100644 --- a/src/frontend/title_info_dialog.ui +++ b/src/frontend/title_info_dialog.ui @@ -1,238 +1,238 @@ - - - TitleInfoDialog - - - Title Info - - - - - - Info - - - - - - Version: - - - - - - - true - - - - - - - - 48 - 48 - - - - Qt::ActionsContextMenu - - - - - - - - 24 - 24 - - - - Qt::ActionsContextMenu - - - - - - - Encryption: - - - - - - - true - - - - - - - Title ID: - - - - - - - true - - - - - - - - - - Names - - - - - - Short Title: - - - - - - - true - - - - - - - - - - Long Title: - - - - - - - true - - - - - - - Publisher: - - - - - - - true - - - - - - - - - - Checks - - - - - - TMD: - - - - - - - Legit - - - - - - - Qt::Horizontal - - - - - - - Ticket: - - - - - - - Legit - - - - - - - Qt::Horizontal - - - - - - - Contents: - - - - - - - Check - - - - - - - false - - - Legit - - - - - - - Qt::Horizontal - - - - - - - - - - Qt::Vertical - - - - - - - QDialogButtonBox::Ok - - - - - - + + + TitleInfoDialog + + + Title Info + + + + + + Info + + + + + + Version: + + + + + + + true + + + + + + + + 48 + 48 + + + + Qt::ActionsContextMenu + + + + + + + + 24 + 24 + + + + Qt::ActionsContextMenu + + + + + + + Encryption: + + + + + + + true + + + + + + + Title ID: + + + + + + + true + + + + + + + + + + Names + + + + + + Short Title: + + + + + + + true + + + + + + + + + + Long Title: + + + + + + + true + + + + + + + Publisher: + + + + + + + true + + + + + + + + + + Checks + + + + + + TMD: + + + + + + + Legit + + + + + + + Qt::Horizontal + + + + + + + Ticket: + + + + + + + Legit + + + + + + + Qt::Horizontal + + + + + + + Contents: + + + + + + + Check + + + + + + + false + + + Legit + + + + + + + Qt::Horizontal + + + + + + + + + + Qt::Vertical + + + + + + + QDialogButtonBox::Ok + + + + + + diff --git a/src/frontend/utilities.cpp b/src/frontend/utilities.cpp index bf7cc1a..9892c0b 100644 --- a/src/frontend/utilities.cpp +++ b/src/frontend/utilities.cpp @@ -1,302 +1,302 @@ -// Copyright 2020 threeSD Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include -#include -#include -#include -#include -#include -#include "core/file_sys/data/data_container.h" -#include "core/file_sys/data/extdata.h" -#include "core/file_sys/data/savegame.h" -#include "core/file_sys/ncch_container.h" -#include "core/key/key.h" -#include "core/sdmc_decryptor.h" -#include "frontend/select_files_dialog.h" -#include "frontend/utilities.h" -#include "ui_utilities.h" - -UtilitiesDialog::UtilitiesDialog(QWidget* parent) - : DPIAwareDialog(parent, 640, 384), ui(std::make_unique()) { - - ui->setupUi(this); - setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint)); - - 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->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->setWindowFlags(dialog->windowFlags() & (~Qt::WindowContextHelpButtonHint)); - 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 = sdmc_root, relative_source = relative_source, destination = 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 = sdmc_root, relative_source = relative_source, - source = source, destination = 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; - } - - std::vector> container_data; - if (!container.GetIVFCLevel4Data(container_data)) { - return false; - } - - Core::Savegame save(std::move(container_data)); - if (!save.IsGood()) { - return false; - } - - return save.Extract(destination.toStdString()); - }); - } else { - // TODO: Add Progress reporting - ShowProgressDialog([source = source, destination = destination] { - FileUtil::IOFile file(source.toStdString(), "rb"); - std::vector data = file.GetData(); - if (data.empty()) { - return false; - } - - Core::DataContainer container(data); - if (!container.IsGood()) { - return false; - } - - std::vector> container_data; - if (!container.GetIVFCLevel4Data(container_data)) { - return false; - } - - Core::Savegame save(std::move(container_data)); - 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 = sdmc_root, relative_source = relative_source, destination = destination] { - Core::SDMCDecryptor decryptor(sdmc_root); - Core::Extdata 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 = source, destination = destination] { - FileUtil::IOFile src_file(source.toStdString(), "rb"); - std::vector data = src_file.GetData(); - if (data.empty()) { - return false; - } - - const auto& shared_romfs = Core::LoadSharedRomFS(data); - return FileUtil::WriteBytesToFile(destination.toStdString(), shared_romfs.data(), - shared_romfs.size()); - }); -} - -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.")); - } -} +// Copyright 2020 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include +#include "core/file_sys/data/data_container.h" +#include "core/file_sys/data/extdata.h" +#include "core/file_sys/data/savegame.h" +#include "core/file_sys/ncch_container.h" +#include "core/key/key.h" +#include "core/sdmc_decryptor.h" +#include "frontend/select_files_dialog.h" +#include "frontend/utilities.h" +#include "ui_utilities.h" + +UtilitiesDialog::UtilitiesDialog(QWidget* parent) + : DPIAwareDialog(parent, 640, 384), ui(std::make_unique()) { + + ui->setupUi(this); + setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint)); + + 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->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->setWindowFlags(dialog->windowFlags() & (~Qt::WindowContextHelpButtonHint)); + 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 = sdmc_root, relative_source = relative_source, destination = 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 = sdmc_root, relative_source = relative_source, + source = source, destination = 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; + } + + std::vector> container_data; + if (!container.GetIVFCLevel4Data(container_data)) { + return false; + } + + Core::Savegame save(std::move(container_data)); + if (!save.IsGood()) { + return false; + } + + return save.Extract(destination.toStdString()); + }); + } else { + // TODO: Add Progress reporting + ShowProgressDialog([source = source, destination = destination] { + FileUtil::IOFile file(source.toStdString(), "rb"); + std::vector data = file.GetData(); + if (data.empty()) { + return false; + } + + Core::DataContainer container(data); + if (!container.IsGood()) { + return false; + } + + std::vector> container_data; + if (!container.GetIVFCLevel4Data(container_data)) { + return false; + } + + Core::Savegame save(std::move(container_data)); + 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 = sdmc_root, relative_source = relative_source, destination = destination] { + Core::SDMCDecryptor decryptor(sdmc_root); + Core::Extdata 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 = source, destination = destination] { + FileUtil::IOFile src_file(source.toStdString(), "rb"); + std::vector data = src_file.GetData(); + if (data.empty()) { + return false; + } + + const auto& shared_romfs = Core::LoadSharedRomFS(data); + return FileUtil::WriteBytesToFile(destination.toStdString(), shared_romfs.data(), + shared_romfs.size()); + }); +} + +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 index a420695..182d310 100644 --- a/src/frontend/utilities.h +++ b/src/frontend/utilities.h @@ -1,49 +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 "frontend/helpers/dpi_aware_dialog.h" - -class QWidget; - -namespace Ui { -class UtilitiesDialog; -} - -class UtilitiesDialog : public DPIAwareDialog { - 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; -}; +// Copyright 2020 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include "frontend/helpers/dpi_aware_dialog.h" + +class QWidget; + +namespace Ui { +class UtilitiesDialog; +} + +class UtilitiesDialog : public DPIAwareDialog { + 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 index 6e42eed..d65972c 100644 --- a/src/frontend/utilities.ui +++ b/src/frontend/utilities.ui @@ -1,251 +1,251 @@ - - - UtilitiesDialog - - - threeSD Utilities - - - - - - Encryption - - - - - - Use SD Decryption (check this when your files are directly from SD Card) - - - true - - - - - - - - - boot9.bin - - - - - - - - - - ... - - - - - - - movable.sed - - - - - - - - - - ... - - - - - - - SDMC Root - - - Path to "Nintendo 3DS/<ID0>/<ID1>" folder. - - - - - - - - - - ... - - - - - - - - - - - - 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 - - - - - - - - - + + + UtilitiesDialog + + + threeSD Utilities + + + + + + Encryption + + + + + + Use SD Decryption (check this when your files are directly from SD Card) + + + true + + + + + + + + + boot9.bin + + + + + + + + + + ... + + + + + + + movable.sed + + + + + + + + + + ... + + + + + + + SDMC Root + + + Path to "Nintendo 3DS/<ID0>/<ID1>" folder. + + + + + + + + + + ... + + + + + + + + + + + + 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 + + + + + + + + +