Add support for title.db, and use title.db to augment TMD finding

Also added support for system title CIA building, and fixed various minor issues.
Also moved MakeMagic to Common.
This commit is contained in:
Pengfei
2021-07-01 14:13:20 +08:00
parent 857bd12a6f
commit e2bef4d705
16 changed files with 326 additions and 88 deletions
+9
View File
@@ -78,3 +78,12 @@ bool CheckedMemcpy(void* dest, T& container, std::ptrdiff_t offset, std::size_t
std::memcpy(dest, container.data() + offset, size);
return true;
}
consteval u32 MakeMagic(char a, char b, char c, char d) {
return a | b << 8 | c << 16 | d << 24;
}
consteval u64 MakeMagic(char a, char b, char c, char d, char e, char f, char g, char h) {
return u64(a) | u64(b) << 8 | u64(c) << 16 | u64(d) << 24 | u64(e) << 32 | u64(f) << 40 |
u64(g) << 48 | u64(h) << 56;
}
+1
View File
@@ -40,3 +40,4 @@
#define SEED_DB "seeddb.bin"
#define AES_KEYS "aes_keys.txt"
#define CERTS_DB "certs.db"
#define TITLE_DB "title.db"
+2
View File
@@ -26,6 +26,8 @@ add_library(core STATIC
quick_decryptor.cpp
quick_decryptor.h
result_status.h
title_db.cpp
title_db.h
)
target_link_libraries(core PRIVATE common cryptopp)
-4
View File
@@ -9,10 +9,6 @@
namespace Core {
constexpr u32 MakeMagic(char a, char b, char c, char d) {
return a | b << 8 | c << 16 | d << 24;
}
DPFSContainer::DPFSContainer(DPFSDescriptor descriptor_, u8 level1_selector_,
std::vector<u32_le> data_)
: descriptor(descriptor_), level1_selector(level1_selector_), data(std::move(data_)) {
+107 -65
View File
@@ -17,6 +17,7 @@
#include "core/ncch/seed_db.h"
#include "core/ncch/smdh.h"
#include "core/ncch/title_metadata.h"
#include "core/title_db.h"
namespace Core {
@@ -56,6 +57,33 @@ bool SDMCImporter::Init() {
decryptor = std::make_unique<SDMCDecryptor>(config.sdmc_path);
cia_builder = std::make_unique<CIABuilder>();
// Load SDMC Title DB
{
DataContainer container(decryptor->DecryptFile("/dbs/title.db"));
std::vector<std::vector<u8>> data;
if (container.IsGood() && container.GetIVFCLevel4Data(data)) {
sdmc_title_db = std::make_unique<TitleDB>(std::move(data[0]));
}
}
if (!sdmc_title_db || !sdmc_title_db->IsGood()) {
LOG_WARNING(Core, "SDMC title.db invalid");
sdmc_title_db.reset();
}
// Load NAND Title DB
if (!config.nand_title_db_path.empty()) {
FileUtil::IOFile file(config.nand_title_db_path, "rb");
DataContainer container(file.GetData());
std::vector<std::vector<u8>> data;
if (container.IsGood() && container.GetIVFCLevel4Data(data)) {
nand_title_db = std::make_unique<TitleDB>(std::move(data[0]));
}
}
if (!nand_title_db || !nand_title_db->IsGood()) {
LOG_WARNING(Core, "NAND title.db invalid");
nand_title_db.reset();
}
FileUtil::SetUserPath(config.user_path);
return true;
}
@@ -426,17 +454,22 @@ static std::string FindTMD(const std::string& path) {
return true;
}
if (virtual_name.substr(virtual_name.size() - 3) == "tmd" &&
if (virtual_name.size() == 12 &&
virtual_name.substr(virtual_name.size() - 4) == ".tmd" &&
std::regex_match(virtual_name.substr(0, 8), title_regex)) {
title_metadata = virtual_name;
return false;
// We would like to find the TMD with the smallest content ID,
// as that would be the finalized version, not the version
// pending installation
title_metadata =
title_metadata.empty() ? virtual_name : std::min(title_metadata, virtual_name);
return true;
}
return true;
});
if (ret) { // TMD not found
if (title_metadata.empty()) { // TMD not found
return {};
}
@@ -447,16 +480,39 @@ static std::string FindTMD(const std::string& path) {
return path + title_metadata;
}
static bool LoadTMD(const std::string& sdmc_path, const std::string& path, SDMCDecryptor& decryptor,
TitleMetadata& out) {
bool SDMCImporter::LoadTMD(ContentType type, u64 id, TitleMetadata& out) const {
const bool is_nand = type == ContentType::SystemTitle;
const auto tmd = FindTMD(sdmc_path + path.substr(1));
if (tmd.empty()) {
return false;
auto& title_db = is_nand ? nand_title_db : sdmc_title_db;
const auto physical_path =
is_nand ? fmt::format("{}{:08x}/{:08x}/content/", config.system_titles_path, (id >> 32),
(id & 0xFFFFFFFF))
: fmt::format("{}title/{:08x}/{:08x}/content/", config.sdmc_path, (id >> 32),
(id & 0xFFFFFFFF));
std::string tmd_path;
if (title_db && title_db->titles.count(id)) {
tmd_path =
fmt::format("{}{:08x}.tmd", physical_path, title_db->titles.at(id).tmd_content_id);
} else {
LOG_WARNING(Core, "Title {:016x} does not exist in title.db", id);
tmd_path = FindTMD(physical_path);
if (tmd_path.empty()) {
return false;
}
}
return out.Load(decryptor.DecryptFile(tmd.substr(sdmc_path.size() - 1))) ==
ResultStatus::Success;
if (is_nand) {
FileUtil::IOFile file(tmd_path, "rb");
if (!file || file.GetSize() > 1024 * 1024) {
LOG_ERROR(Core, "Could not open {} or file too big", tmd_path);
return false;
}
return out.Load(file.GetData()) == ResultStatus::Success;
} else {
return out.Load(decryptor->DecryptFile(tmd_path.substr(config.sdmc_path.size() - 1))) ==
ResultStatus::Success;
}
}
// English short title name, extdata id, encryption, seed, icon
@@ -513,16 +569,14 @@ bool SDMCImporter::DumpCXI(const ContentSpecifier& specifier, const std::string&
return false;
}
const auto content_path = fmt::format("/title/{:08x}/{:08x}/content/", specifier.id >> 32,
(specifier.id & 0xFFFFFFFF));
TitleMetadata tmd;
if (!LoadTMD(config.sdmc_path, content_path, *decryptor, tmd)) {
LOG_ERROR(Core, "Could not load tmd");
if (!LoadTMD(specifier.type, specifier.id, tmd)) {
return false;
}
const auto boot_content_path =
fmt::format("{}{:08x}.app", content_path, tmd.GetBootContentID());
fmt::format("/title/{:08x}/{:08x}/content/{:08x}.app", specifier.id >> 32,
(specifier.id & 0xFFFFFFFF), tmd.GetBootContentID());
dump_cxi_ncch = std::make_unique<NCCHContainer>(
std::make_shared<SDMCFile>(config.sdmc_path, boot_content_path, "rb"));
@@ -548,32 +602,35 @@ bool SDMCImporter::BuildCIA(const ContentSpecifier& specifier, const std::string
}
if (specifier.type != ContentType::Application && specifier.type != ContentType::Update &&
specifier.type != ContentType::DLC) {
specifier.type != ContentType::DLC && specifier.type != ContentType::SystemTitle) {
LOG_ERROR(Core, "Unsupported specifier type {}", static_cast<int>(specifier.type));
return false;
}
// Load TMD
const auto path = fmt::format("/title/{:08x}/{:08x}/content/", (specifier.id >> 32),
(specifier.id & 0xFFFFFFFF));
TitleMetadata tmd;
if (!LoadTMD(config.sdmc_path, path, *decryptor, tmd)) {
LOG_ERROR(Core, "Failed to load TMD from {}", path);
if (!LoadTMD(specifier.type, specifier.id, tmd)) {
return false;
}
const auto physical_path = config.sdmc_path + path.substr(1);
bool ret = cia_builder->Init(destination, std::move(tmd), config.certs_db_path,
const bool is_nand = specifier.type == ContentType::SystemTitle;
const auto physical_path =
is_nand ? fmt::format("{}{:08x}/{:08x}/content/", config.system_titles_path,
(specifier.id >> 32), (specifier.id & 0xFFFFFFFF))
: fmt::format("{}title/{:08x}/{:08x}/content/", config.sdmc_path,
(specifier.id >> 32), (specifier.id & 0xFFFFFFFF));
bool ret = cia_builder->Init(destination, tmd, config.certs_db_path,
FileUtil::GetDirectoryTreeSize(physical_path), callback);
if (!ret) {
return false;
}
const FileUtil::DirectoryEntryCallable DirectoryEntryCallback =
[this, specifier, path, &DirectoryEntryCallback](u64* /*num_entries_out*/,
const std::string& directory,
const std::string& virtual_name) {
[this, tmd, is_nand, specifier, &DirectoryEntryCallback](u64* /*num_entries_out*/,
const std::string& directory,
const std::string& virtual_name) {
if (FileUtil::IsDirectory(directory + virtual_name + "/")) {
if (virtual_name == "cmd") {
return true; // Skip cmd (not used in Citra)
@@ -592,9 +649,22 @@ bool SDMCImporter::BuildCIA(const ContentSpecifier& specifier, const std::string
ASSERT(match.size() >= 2);
const u32 id = static_cast<u32>(std::stoul(match[1], nullptr, 16));
const auto relative_path = directory.substr(config.sdmc_path.size() - 1) + virtual_name;
NCCHContainer ncch(std::make_shared<SDMCFile>(config.sdmc_path, relative_path, "rb"));
return cia_builder->AddContent(id, ncch);
if (!tmd.HasContentID(id)) {
LOG_WARNING(Core, "Ignoring content {} (not in TMD)", directory + virtual_name);
return true;
}
if (is_nand) {
NCCHContainer ncch(
std::make_shared<FileUtil::IOFile>(directory + virtual_name, "rb"));
return cia_builder->AddContent(id, ncch);
} else {
const auto relative_path =
directory.substr(config.sdmc_path.size() - 1) + virtual_name;
NCCHContainer ncch(
std::make_shared<SDMCFile>(config.sdmc_path, relative_path, "rb"));
return cia_builder->AddContent(id, ncch);
}
};
if (!FileUtil::ForeachDirectoryEntry(nullptr, physical_path, DirectoryEntryCallback)) {
@@ -633,12 +703,8 @@ void SDMCImporter::ListTitle(std::vector<ContentSpecifier>& out) const {
if (FileUtil::Exists(directory + virtual_name + "/content/")) {
do {
const auto content_path =
fmt::format("/title/{:08x}/{}/content/", high_id, virtual_name);
TitleMetadata tmd;
if (!LoadTMD(sdmc_path, content_path, *decryptor, tmd)) {
LOG_WARNING(Core, "Could not load tmd from {}", content_path);
if (!LoadTMD(type, id, tmd)) {
out.push_back({type, id, FileUtil::Exists(citra_path + "content/"),
FileUtil::GetDirectoryTreeSize(directory + virtual_name +
"/content/")});
@@ -646,7 +712,8 @@ void SDMCImporter::ListTitle(std::vector<ContentSpecifier>& out) const {
}
const auto boot_content_path =
fmt::format("{}{:08x}.app", content_path, tmd.GetBootContentID());
fmt::format("/title/{:08x}/{}/content/{:08x}.app", high_id,
virtual_name, tmd.GetBootContentID());
NCCHContainer ncch(
std::make_shared<SDMCFile>(sdmc_path, boot_content_path, "rb"));
if (ncch.Load() != ResultStatus::Success) {
@@ -692,39 +759,14 @@ void SDMCImporter::ListTitle(std::vector<ContentSpecifier>& out) const {
ProcessDirectory(ContentType::DLC, 0x0004008c);
}
static bool LoadNandTMD(const std::string& path, TitleMetadata& out) {
const auto tmd = FindTMD(path);
if (tmd.empty()) {
return false;
}
FileUtil::IOFile file(tmd, "rb");
if (!file) {
LOG_ERROR(Core, "Could not open file {}", tmd);
return false;
}
if (file.GetSize() >= 1024 * 1024) { // Too big
LOG_ERROR(Core, "TMD {} too big", tmd);
return false;
}
std::vector<u8> data(file.GetSize());
if (file.ReadBytes(data.data(), data.size()) != data.size()) {
LOG_ERROR(Core, "Could not read from {}", tmd);
return false;
}
return out.Load(std::move(data)) == ResultStatus::Success;
}
// TODO: Simplify.
void SDMCImporter::ListNandTitle(std::vector<ContentSpecifier>& out) const {
const auto ProcessDirectory = [&out,
const auto ProcessDirectory = [this, &out,
&system_titles_path = config.system_titles_path](u64 high_id) {
FileUtil::ForeachDirectoryEntry(
nullptr, fmt::format("{}{:08x}/", system_titles_path, high_id),
[high_id, &out](u64* /*num_entries_out*/, const std::string& directory,
const std::string& virtual_name) {
[this, high_id, &out](u64* /*num_entries_out*/, const std::string& directory,
const std::string& virtual_name) {
if (!FileUtil::IsDirectory(directory + virtual_name + "/")) {
return true;
}
@@ -742,8 +784,7 @@ void SDMCImporter::ListNandTitle(std::vector<ContentSpecifier>& out) const {
if (FileUtil::Exists(content_path)) {
do {
TitleMetadata tmd;
if (!LoadNandTMD(content_path, tmd)) {
LOG_WARNING(Core, "Could not load tmd from {}", content_path);
if (!LoadTMD(ContentType::SystemTitle, id, tmd)) {
out.push_back({ContentType::SystemTitle, id,
FileUtil::Exists(citra_path + "content/"),
FileUtil::GetDirectoryTreeSize(content_path)});
@@ -1085,6 +1126,7 @@ std::vector<Config> LoadPresetConfig(std::string mount_point) {
LOAD_DATA(movable_sed_path, MOVABLE_SED);
LOAD_DATA(bootrom_path, BOOTROM9);
LOAD_DATA(certs_db_path, CERTS_DB);
LOAD_DATA(nand_title_db_path, TITLE_DB);
LOAD_DATA(safe_mode_firm_path, "firm/");
LOAD_DATA(seed_db_path, SEED_DB);
LOAD_DATA(secret_sector_path, SECRET_SECTOR);
+10 -2
View File
@@ -15,6 +15,8 @@ namespace Core {
class CIABuilder;
class SDMCDecryptor;
class TitleDB;
class TitleMetadata;
/**
* Type of an importable content.
@@ -71,7 +73,8 @@ struct Config {
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.
std::string certs_db_path; ///< Path to certs.db. Used while building CIA.
std::string nand_title_db_path; ///< Path to NAND title.db. Entirely optional.
// The following system files are optional for importing and are only copied so that Citra
// will be able to decrypt imported encrypted ROMs.
@@ -90,7 +93,7 @@ struct Config {
};
// Version of the current dumper.
constexpr int CurrentDumperVersion = 3;
constexpr int CurrentDumperVersion = 4;
class SDMCFile;
class NCCHContainer;
@@ -190,6 +193,8 @@ private:
void DeleteSystemArchive(u64 id) const;
void DeleteSysdata(u64 id) const;
bool LoadTMD(ContentType type, u64 id, TitleMetadata& out) const;
bool is_good{};
Config config;
std::unique_ptr<SDMCDecryptor> decryptor;
@@ -197,6 +202,9 @@ private:
// The NCCH used to dump CXIs.
std::unique_ptr<NCCHContainer> dump_cxi_ncch;
std::unique_ptr<TitleDB> sdmc_title_db{};
std::unique_ptr<TitleDB> nand_title_db{};
};
/**
-4
View File
@@ -12,10 +12,6 @@
namespace Core {
constexpr u32 MakeMagic(char a, char b, char c, char d) {
return a | b << 8 | c << 16 | d << 24;
}
InnerFAT::~InnerFAT() = default;
bool InnerFAT::IsGood() const {
+1 -1
View File
@@ -175,7 +175,7 @@ bool CIABuilder::AddContent(u16 content_id, NCCHContainer& ncch) {
file->SetHashEnabled(false);
// DLCs do not have a meta
if (tmd_chunk.index != TMDContentIndex::Main || (tmd.GetTitleID() & 0x0004008c'00000000)) {
if (tmd_chunk.index != TMDContentIndex::Main || (tmd.GetTitleID() >> 32) == 0x0004008c) {
return true;
}
-4
View File
@@ -22,10 +22,6 @@
namespace Core {
constexpr u32 MakeMagic(char a, char b, char c, char d) {
return a | b << 8 | c << 16 | d << 24;
}
static const int kMaxSections = 8; ///< Maximum number of sections (files) in an ExeFs
static const int kBlockSize = 0x200; ///< Size of ExeFS blocks (in bytes)
+1 -4
View File
@@ -4,15 +4,12 @@
#include <cstring>
#include <vector>
#include "common/common_funcs.h"
#include "common/common_types.h"
#include "core/ncch/smdh.h"
namespace Core {
constexpr u32 MakeMagic(char a, char b, char c, char d) {
return a | b << 8 | c << 16 | d << 24;
}
// 8x8 Z-Order coordinate from 2D coordinates
static constexpr u32 MortonInterleave(u32 x, u32 y) {
constexpr u32 xlut[] = {0x00, 0x01, 0x04, 0x05, 0x10, 0x11, 0x14, 0x15};
+7
View File
@@ -207,6 +207,13 @@ const TitleMetadata::ContentChunk& TitleMetadata::GetContentChunkByID(u32 conten
return *it;
}
bool TitleMetadata::HasContentID(u32 content_id) const {
const auto it =
std::find_if(tmd_chunks.begin(), tmd_chunks.end(),
[content_id](const ContentChunk& chunk) { return chunk.id == content_id; });
return it != tmd_chunks.end();
}
void TitleMetadata::AddContentChunk(const ContentChunk& chunk) {
tmd_chunks.push_back(chunk);
}
+1
View File
@@ -127,6 +127,7 @@ public:
ContentChunk& GetContentChunkByID(u32 content_id);
const ContentChunk& GetContentChunkByID(u32 content_id) const;
bool HasContentID(u32 content_id) const;
void SetTitleID(u64 title_id);
void SetTitleType(u32 type);
+104
View File
@@ -0,0 +1,104 @@
// Copyright 2021 threeSD Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include "common/logging/log.h"
#include "core/title_db.h"
namespace Core {
TitleDB::TitleDB(std::vector<u8> data) {
is_good = Init(std::move(data));
}
bool TitleDB::IsGood() const {
return is_good;
}
// Note: Title DB is actually a degenerate version of the Inner FAT.
// We are simplifying things as much as possible and not actually dealing with FAT nodes.
bool TitleDB::Init(std::vector<u8> data) {
// Read header, FAT header and filesystem information
TRY(CheckedMemcpy(&header, data, 0, sizeof(header)), LOG_ERROR(Core, "File size is too small"));
if (header.db_magic != MakeMagic('N', 'A', 'N', 'D', 'T', 'D', 'B', 0) &&
header.db_magic != MakeMagic('T', 'E', 'M', 'P', 'T', 'D', 'B', 0)) {
LOG_ERROR(Core, "File is invalid, decryption errors may have happened.");
return false;
}
if (header.fat_header.magic != MakeMagic('B', 'D', 'R', 'I') ||
header.fat_header.version != 0x30000) {
LOG_ERROR(Core, "File is invalid, decryption errors may have happened.");
return false;
}
if (header.fat_header.image_block_size != 0x80 ||
header.fs_info.data_region_block_size != 0x80) { // This simplifies things
LOG_ERROR(Core, "Unexpected block size (this may be a bug)");
return false;
}
// Read file entry table
file_entry_table.resize(header.fs_info.maximum_file_count + 1); // including head
auto file_entry_table_pos = TitleDBPreheaderSize + header.fs_info.data_region_offset +
header.fs_info.file_entry_table.duplicate.block_index *
static_cast<std::size_t>(header.fs_info.data_region_block_size);
TRY(CheckedMemcpy(file_entry_table.data(), data, file_entry_table_pos,
file_entry_table.size() * sizeof(TitleDBFileEntryTableEntry)),
LOG_ERROR(Core, "File is too small"));
// Read directory entry table for first file index
auto first_file_index_pos =
TitleDBPreheaderSize + header.fs_info.data_region_offset +
header.fs_info.directory_entry_table.duplicate.block_index *
static_cast<std::size_t>(header.fs_info.data_region_block_size) +
0x20 /* sizeof TitleDB's directory entry (to skip head) */ +
0x0C /* offset of first_file_index in directory entry of Title DB */;
if (data.size() < first_file_index_pos + 4) {
LOG_ERROR(Core, "File size is too small");
return false;
}
const u32 first_file_index = *reinterpret_cast<u32_le*>(data.data() + first_file_index_pos);
LOG_INFO(Core, "First file index is {}", first_file_index);
u32 cur = first_file_index;
while (cur != 0) {
if (!LoadTitleInfo(data, cur)) {
return false;
}
cur = file_entry_table[cur].next_sibling_index;
}
return true;
}
bool TitleDB::LoadTitleInfo(const std::vector<u8>& data, u32 index) {
auto entry = file_entry_table[index];
u32 block = entry.data_block_index;
if (block == 0x80000000) { // empty file
LOG_ERROR(Core, "Entry is an empty file");
return false;
}
u64 file_size = entry.file_size;
if (file_size != sizeof(TitleInfoEntry)) {
LOG_ERROR(Core, "Entry has incorrect size {}", file_size);
return false;
}
TitleInfoEntry title;
const auto offset = TitleDBPreheaderSize + header.fs_info.data_region_offset +
block * header.fs_info.data_region_block_size;
TRY(CheckedMemcpy(&title, data, offset, sizeof(TitleInfoEntry)),
LOG_ERROR(Core, "File size is too small"));
titles.emplace(entry.title_id, title);
return true;
}
} // namespace Core
+75
View File
@@ -0,0 +1,75 @@
// Copyright 2021 threeSD Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <array>
#include <unordered_map>
#include <vector>
#include "common/common_funcs.h"
#include "common/common_types.h"
#include "common/swap.h"
#include "core/inner_fat.h"
namespace Core {
struct TitleDBHeader {
u64_le db_magic;
INSERT_PADDING_BYTES(0x78);
FATHeader fat_header;
FileSystemInformation fs_info;
};
constexpr std::size_t TitleDBPreheaderSize = 0x80;
static_assert(sizeof(TitleDBHeader) ==
TitleDBPreheaderSize + sizeof(FATHeader) + sizeof(FileSystemInformation),
"TitleDB preheader has incorrect size");
#pragma pack(push, 1)
struct TitleDBFileEntryTableEntry {
u32_le parent_directory_index;
u64_le title_id;
u32_le next_sibling_index;
INSERT_PADDING_BYTES(4);
u32_le data_block_index;
u64_le file_size;
INSERT_PADDING_BYTES(8);
u32_le next_hash_bucket_entry;
};
static_assert(sizeof(TitleDBFileEntryTableEntry) == 0x2c,
"TitleDBFileEntryTableEntry has incorrect size");
#pragma pack(pop)
struct TitleInfoEntry {
u64_le title_size;
u32_le title_type;
u32_le title_version;
u32_le flags0;
u32_le tmd_content_id;
u32_le cmd_content_id;
u32_le flags1;
u32_le extdata_id_low;
INSERT_PADDING_BYTES(4);
u64_le flags2;
std::array<u8, 0x10> product_code;
INSERT_PADDING_BYTES(0x40);
};
static_assert(sizeof(TitleInfoEntry) == 0x80, "TitleInfoEntry has incorrect size");
class TitleDB {
public:
explicit TitleDB(std::vector<u8> data);
bool IsGood() const;
std::unordered_map<u64, TitleInfoEntry> titles;
private:
bool Init(std::vector<u8> data);
bool LoadTitleInfo(const std::vector<u8>& data, u32 index);
bool is_good = false;
TitleDBHeader header;
std::vector<TitleDBFileEntryTableEntry> file_entry_table;
};
} // namespace Core
+4 -3
View File
@@ -843,19 +843,20 @@ void ImportDialog::StartBatchBuildingCIA() {
to_import.begin(), to_import.end(), [](const Core::ContentSpecifier& specifier) {
return specifier.type != Core::ContentType::Application &&
specifier.type != Core::ContentType::Update &&
specifier.type != Core::ContentType::DLC;
specifier.type != Core::ContentType::DLC &&
specifier.type != Core::ContentType::SystemTitle;
});
if (removed_iter == to_import.begin()) { // No Titles selected
QMessageBox::critical(this, tr("threeSD"),
tr("The contents selected are not supported.<br>You can only build "
"CIAs from Applications, Updates and DLCs."));
"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.<br>Only "
"Applications, Updates and DLCs will be built as CIAs."));
"Applications, Updates, DLCs and System Titles will be built as CIAs."));
}
to_import.erase(removed_iter, to_import.end());