diff --git a/dist/threeSDumper.gm9 b/dist/threeSDumper.gm9 index 57e2bf8..c89d72d 100644 --- a/dist/threeSDumper.gm9 +++ b/dist/threeSDumper.gm9 @@ -39,6 +39,9 @@ end # === ticket.db cp -w -n "1:/dbs/ticket.db" $[OUT]/ticket.db +# === title.db +cp -w -n "1:/dbs/title.db" $[OUT]/title.db + # === Safe mode firm if not find $[OUT]/firm NULL mkdir $[OUT]/firm @@ -187,7 +190,7 @@ else end # === Write version -dumptxt $[OUT]/version.txt 3 +dumptxt $[OUT]/version.txt 4 set PREVIEW_MODE "threeSD Dumper\nby zhaowenlan1779\n \nSuccess!" echo "Successfully dumped necessary\nfiles for threeSD." diff --git a/src/common/common_funcs.h b/src/common/common_funcs.h index bc4115b..fad2c6b 100644 --- a/src/common/common_funcs.h +++ b/src/common/common_funcs.h @@ -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; +} diff --git a/src/common/common_paths.h b/src/common/common_paths.h index 0e1d38e..e788224 100644 --- a/src/common/common_paths.h +++ b/src/common/common_paths.h @@ -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" diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index b3a9db8..41bda06 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -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) diff --git a/src/core/data_container.cpp b/src/core/data_container.cpp index ea93a32..11c2ede 100644 --- a/src/core/data_container.cpp +++ b/src/core/data_container.cpp @@ -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 data_) : descriptor(descriptor_), level1_selector(level1_selector_), data(std::move(data_)) { diff --git a/src/core/importer.cpp b/src/core/importer.cpp index b58f7ef..71426b4 100644 --- a/src/core/importer.cpp +++ b/src/core/importer.cpp @@ -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(config.sdmc_path); cia_builder = std::make_unique(); + // Load SDMC Title DB + { + DataContainer container(decryptor->DecryptFile("/dbs/title.db")); + std::vector> data; + if (container.IsGood() && container.GetIVFCLevel4Data(data)) { + sdmc_title_db = std::make_unique(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> data; + if (container.IsGood() && container.GetIVFCLevel4Data(data)) { + nand_title_db = std::make_unique(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( std::make_shared(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(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(std::stoul(match[1], nullptr, 16)); - const auto relative_path = directory.substr(config.sdmc_path.size() - 1) + virtual_name; - NCCHContainer ncch(std::make_shared(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(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(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& 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& 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(sdmc_path, boot_content_path, "rb")); if (ncch.Load() != ResultStatus::Success) { @@ -692,39 +759,14 @@ void SDMCImporter::ListTitle(std::vector& 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 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& 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& 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 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); diff --git a/src/core/importer.h b/src/core/importer.h index e2f3db2..ae2535d 100644 --- a/src/core/importer.h +++ b/src/core/importer.h @@ -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 decryptor; @@ -197,6 +202,9 @@ private: // The NCCH used to dump CXIs. std::unique_ptr dump_cxi_ncch; + + std::unique_ptr sdmc_title_db{}; + std::unique_ptr nand_title_db{}; }; /** diff --git a/src/core/inner_fat.cpp b/src/core/inner_fat.cpp index 4c94816..4ce148e 100644 --- a/src/core/inner_fat.cpp +++ b/src/core/inner_fat.cpp @@ -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 { diff --git a/src/core/ncch/cia_builder.cpp b/src/core/ncch/cia_builder.cpp index 98f4860..9a8956b 100644 --- a/src/core/ncch/cia_builder.cpp +++ b/src/core/ncch/cia_builder.cpp @@ -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; } diff --git a/src/core/ncch/ncch_container.cpp b/src/core/ncch/ncch_container.cpp index 9773151..e2d6c63 100644 --- a/src/core/ncch/ncch_container.cpp +++ b/src/core/ncch/ncch_container.cpp @@ -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) diff --git a/src/core/ncch/smdh.cpp b/src/core/ncch/smdh.cpp index 54b4e7c..011799a 100644 --- a/src/core/ncch/smdh.cpp +++ b/src/core/ncch/smdh.cpp @@ -4,15 +4,12 @@ #include #include +#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}; diff --git a/src/core/ncch/title_metadata.cpp b/src/core/ncch/title_metadata.cpp index b528bc3..02ffdba 100644 --- a/src/core/ncch/title_metadata.cpp +++ b/src/core/ncch/title_metadata.cpp @@ -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); } diff --git a/src/core/ncch/title_metadata.h b/src/core/ncch/title_metadata.h index 3c95a6f..7ace44c 100644 --- a/src/core/ncch/title_metadata.h +++ b/src/core/ncch/title_metadata.h @@ -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); diff --git a/src/core/title_db.cpp b/src/core/title_db.cpp new file mode 100644 index 0000000..a49a01f --- /dev/null +++ b/src/core/title_db.cpp @@ -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 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 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(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(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(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& 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 diff --git a/src/core/title_db.h b/src/core/title_db.h new file mode 100644 index 0000000..47eb01e --- /dev/null +++ b/src/core/title_db.h @@ -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 +#include +#include +#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 product_code; + INSERT_PADDING_BYTES(0x40); +}; +static_assert(sizeof(TitleInfoEntry) == 0x80, "TitleInfoEntry has incorrect size"); + +class TitleDB { +public: + explicit TitleDB(std::vector data); + bool IsGood() const; + + std::unordered_map titles; + +private: + bool Init(std::vector data); + bool LoadTitleInfo(const std::vector& data, u32 index); + + bool is_good = false; + TitleDBHeader header; + std::vector file_entry_table; +}; + +} // namespace Core diff --git a/src/frontend/import_dialog.cpp b/src/frontend/import_dialog.cpp index 05af983..d87c7b5 100644 --- a/src/frontend/import_dialog.cpp +++ b/src/frontend/import_dialog.cpp @@ -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.
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.
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());