From b19803c4d3d15b2225d7539a725db23b3d04ca82 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Thu, 14 May 2020 23:14:59 +0800 Subject: [PATCH] Add support for system titles --- dist/threeSDumper.gm9 | 55 ++++++- src/core/importer.cpp | 251 +++++++++++++++++++++++++------ src/core/importer.h | 19 ++- src/core/ncch/ncch_container.cpp | 57 ++++--- src/core/ncch/ncch_container.h | 16 +- src/frontend/import_dialog.cpp | 37 +++-- src/frontend/import_dialog.h | 4 +- 7 files changed, 334 insertions(+), 105 deletions(-) diff --git a/dist/threeSDumper.gm9 b/dist/threeSDumper.gm9 index bda721d..81609ea 100644 --- a/dist/threeSDumper.gm9 +++ b/dist/threeSDumper.gm9 @@ -16,7 +16,10 @@ end set PREVIEW_MODE "threeSD Dumper\nby zhaowenlan1779\n \nWorking..." +# === movable.sed cp -w -n "1:/private/movable.sed" $[OUT]/movable.sed + +# === bootrom if find "M:/boot9.bin" NULL cp -w -n "M:/boot9.bin" $[OUT]/boot9.bin elif find "0:/3DS/boot9.bin" NULL @@ -26,6 +29,7 @@ else goto Exit end +# === Safe mode firm if not find $[OUT]/firm NULL mkdir $[OUT]/firm end @@ -47,10 +51,12 @@ else decrypt $[APP] end +# === Secret sector (N3DS only) if chk $[ONTYPE] "N3DS" cp -w -n "S:/sector0x96.bin" $[OUT]/sector0x96.bin end +# === System Archives if not find $[OUT]/sysarchives NULL mkdir $[OUT]/sysarchives end @@ -98,10 +104,52 @@ find 1:/title/000400db/00010302/content/*.app APP cp -w -n $[APP] $[OUT]/sysarchives/000400db/00010302.app decrypt $[OUT]/sysarchives/000400db/00010302.app -# Config savegame +# === Config savegame cp -w -n 1:/data/$[SYSID0]/sysdata/00010017/00000000 $[OUT]/config.sav -# seeddb.bin +# === Other system titles +if not find $[OUT]/title NULL + mkdir $[OUT]/title +end + +# System Applications +if not find $[OUT]/title/00040010 NULL + mkdir $[OUT]/title/00040010 +end +cp -w -n "1:/title/00040010" $[OUT]/title/00040010 + +# System Data Archives +if not find $[OUT]/title/0004001b NULL + mkdir $[OUT]/title/0004001b +end +cp -w -n "1:/title/0004001b" $[OUT]/title/0004001b + +# System Applets +if not find $[OUT]/title/00040030 NULL + mkdir $[OUT]/title/00040030 +end +cp -w -n "1:/title/00040030" $[OUT]/title/00040030 + +# 0004009b Shared Data Archives skipped (included in sysarchives) + +# System Data Archives +if not find $[OUT]/title/000400db NULL + mkdir $[OUT]/title/000400db +end +cp -w -n "1:/title/000400db" $[OUT]/title/000400db + +# System Modules +if not find $[OUT]/title/00040130 NULL + mkdir $[OUT]/title/00040130 +end +cp -w -n "1:/title/00040130" $[OUT]/title/00040130 + +# 00040138 System Firmware skipped (dumped above) + +# Already included in sysarchives +rm $[OUT]/title/000400db/00010302 + +# === seeddb.bin sdump -o -s -w seeddb.bin if not find 0:/gm9/out/seeddb.bin NULL echo "WARNING: \nseeddb.bin couldn't be built. \nThis may be because your system \ndoes not have any seeds. \nOtherwise, imported games may fail \nto run if they use seed encryption." @@ -110,6 +158,9 @@ else rm "0:/gm9/out/seeddb.bin" end +# === Write version +dumptxt $[OUT]/version.txt 1 + set PREVIEW_MODE "threeSD Dumper\nby zhaowenlan1779\n \nSuccess!" echo "Successfully dumped necessary\nfiles for threeSD." diff --git a/src/core/importer.cpp b/src/core/importer.cpp index 94165e7..996ff1e 100644 --- a/src/core/importer.cpp +++ b/src/core/importer.cpp @@ -81,18 +81,25 @@ bool SDMCImporter::ImportContent(const ContentSpecifier& specifier, return ImportSystemArchive(specifier.id, callback); case ContentType::Sysdata: return ImportSysdata(specifier.id, callback); + case ContentType::SystemTitle: + return ImportNandTitle(specifier, callback); default: UNREACHABLE(); } } -bool SDMCImporter::ImportTitle(const ContentSpecifier& specifier, - const ProgressCallback& callback) { - decryptor->Reset(specifier.maximum_size); +namespace { + +template +bool ImportTitleGeneric(Dec& decryptor, const std::string& base_path, + const ContentSpecifier& specifier, + const std::function& decryption_func) { + + decryptor.Reset(specifier.maximum_size); const FileUtil::DirectoryEntryCallable DirectoryEntryCallback = - [this, size = config.sdmc_path.size(), callback, - &DirectoryEntryCallback](u64* /*num_entries_out*/, const std::string& directory, - const std::string& virtual_name) { + [size = base_path.size(), &DirectoryEntryCallback, + &decryption_func](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) @@ -102,6 +109,19 @@ bool SDMCImporter::ImportTitle(const ContentSpecifier& specifier, DirectoryEntryCallback); } const auto filepath = (directory + virtual_name).substr(size - 1); + return decryption_func(filepath); + }; + const auto path = fmt::format("title/{:08x}/{:08x}/content/", (specifier.id >> 32), + (specifier.id & 0xFFFFFFFF)); + return FileUtil::ForeachDirectoryEntry(nullptr, base_path + path, DirectoryEntryCallback); +} + +} // namespace + +bool SDMCImporter::ImportTitle(const ContentSpecifier& specifier, + const ProgressCallback& callback) { + return ImportTitleGeneric( + *decryptor, config.sdmc_path, specifier, [this, &callback](const std::string& filepath) { return decryptor->DecryptAndWriteFile( filepath, FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) + @@ -109,11 +129,31 @@ bool SDMCImporter::ImportTitle(const ContentSpecifier& specifier, "3DS/00000000000000000000000000000000/00000000000000000000000000000000" + filepath, callback); - }; - const auto path = fmt::format("title/{:08x}/{:08x}/content/", (specifier.id >> 32), - (specifier.id & 0xFFFFFFFF)); - return FileUtil::ForeachDirectoryEntry(nullptr, config.sdmc_path + path, - DirectoryEntryCallback); + }); +} + +bool SDMCImporter::ImportNandTitle(const ContentSpecifier& specifier, + const ProgressCallback& callback) { + + const auto base_path = + config.system_titles_path.substr(0, config.system_titles_path.size() - 6); + QuickDecryptor<> quick_decryptor; + return ImportTitleGeneric( + quick_decryptor, base_path, specifier, + [&base_path, &quick_decryptor, &callback](const std::string& filepath) { + const auto physical_path = base_path + filepath.substr(1); + const auto citra_path = FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + + "00000000000000000000000000000000" + filepath; + if (!FileUtil::CreateFullPath(citra_path)) { + LOG_ERROR(Core, "Could not create path {}", citra_path); + return false; + } + // Do not specify keys: plain copy with progress. + return quick_decryptor.DecryptAndWriteFile( + std::make_shared(physical_path, "rb"), + FileUtil::GetSize(physical_path), + std::make_shared(citra_path, "wb"), callback); + }); } bool SDMCImporter::ImportSavegame(u64 id, [[maybe_unused]] const ProgressCallback& callback) { @@ -310,6 +350,7 @@ bool SDMCImporter::ImportSysdata(u64 id, [[maybe_unused]] const ProgressCallback std::vector SDMCImporter::ListContent() const { std::vector content_list; ListTitle(content_list); + ListNandTitle(content_list); ListExtdata(content_list); ListSystemArchive(content_list); ListSysdata(content_list); @@ -319,12 +360,10 @@ std::vector SDMCImporter::ListContent() const { // Regex for half Title IDs static const std::regex title_regex{"[0-9a-f]{8}"}; -static bool LoadTMD(const std::string& sdmc_path, const std::string& path, SDMCDecryptor& decryptor, - TitleMetadata& out) { - +static std::string FindTMD(const std::string& path) { std::string title_metadata; const bool ret = FileUtil::ForeachDirectoryEntry( - nullptr, sdmc_path + path, + nullptr, path, [&title_metadata](u64* /*num_entries_out*/, const std::string& directory, const std::string& virtual_name) { if (FileUtil::IsDirectory(directory + virtual_name)) { @@ -342,35 +381,32 @@ static bool LoadTMD(const std::string& sdmc_path, const std::string& path, SDMCD }); if (ret) { // TMD not found - return false; + return {}; } - if (!FileUtil::Exists(sdmc_path + path + title_metadata)) { + if (!FileUtil::Exists(path + title_metadata)) { // Probably TMD is not directly inside, aborting. - return false; + return {}; } - - return out.Load(decryptor.DecryptFile(path + title_metadata)) == ResultStatus::Success; + return path + title_metadata; } -std::tuple> SDMCImporter::LoadTitleData( - const std::string& path) const { - // Remove trailing '/' - const auto sdmc_path = config.sdmc_path.substr(0, config.sdmc_path.size() - 1); +static bool LoadTMD(const std::string& sdmc_path, const std::string& path, SDMCDecryptor& decryptor, + TitleMetadata& out) { - TitleMetadata tmd; - if (!LoadTMD(sdmc_path, path, *decryptor, tmd)) { - return {}; + const auto tmd = FindTMD(sdmc_path + path.substr(1)); + if (tmd.empty()) { + return false; } - const auto boot_content_path = fmt::format("{}{:08x}.app", path, tmd.GetBootContentID()); + return out.Load(decryptor.DecryptFile(tmd.substr(sdmc_path.size() - 1))) == + ResultStatus::Success; +} - NCCHContainer ncch(config.sdmc_path, boot_content_path); - auto ret2 = ncch.Load(); - if (ret2 != ResultStatus::Success) { - LOG_CRITICAL(Core, "failed to load ncch: {}", ret2); - return {}; - } +// English short title name, extdata id, encryption, seed, icon +template +std::tuple> LoadTitleData( + NCCHContainer& ncch) { std::vector smdh_buffer; if (ncch.LoadSectionExeFS("icon", smdh_buffer) != ResultStatus::Success) { @@ -416,7 +452,8 @@ bool SDMCImporter::DumpCXI(const ContentSpecifier& specifier, const std::string& const auto boot_content_path = fmt::format("{}{:08x}.app", content_path, tmd.GetBootContentID()); - dump_cxi_ncch = std::make_unique(config.sdmc_path, boot_content_path); + dump_cxi_ncch = std::make_unique>( + std::make_shared(config.sdmc_path, boot_content_path, "rb")); return dump_cxi_ncch->DecryptToFile(destination, callback) == ResultStatus::Success; } @@ -429,8 +466,9 @@ void SDMCImporter::ListTitle(std::vector& out) const { u64 high_id) { FileUtil::ForeachDirectoryEntry( nullptr, fmt::format("{}title/{:08x}/", sdmc_path, high_id), - [this, type, high_id, &out](u64* /*num_entries_out*/, const std::string& directory, - const std::string& virtual_name) { + [this, &sdmc_path, type, high_id, &out](u64* /*num_entries_out*/, + const std::string& directory, + const std::string& virtual_name) { if (!FileUtil::IsDirectory(directory + virtual_name + "/")) { return true; } @@ -447,14 +485,38 @@ void SDMCImporter::ListTitle(std::vector& out) const { FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir), high_id, virtual_name); if (FileUtil::Exists(directory + virtual_name + "/content/")) { - const auto content_path = - fmt::format("/title/{:08x}/{}/content/", high_id, virtual_name); - const auto& [name, extdata_id, encryption, seed_crypto, icon] = - LoadTitleData(content_path); - out.push_back( - {type, id, FileUtil::Exists(citra_path + "content/"), - FileUtil::GetDirectoryTreeSize(directory + virtual_name + "/content/"), - name, extdata_id, encryption, seed_crypto, icon}); + 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); + out.push_back({type, id, FileUtil::Exists(citra_path + "content/"), + FileUtil::GetDirectoryTreeSize(directory + virtual_name + + "/content/")}); + break; + } + + const auto boot_content_path = + fmt::format("{}{:08x}.app", content_path, tmd.GetBootContentID()); + NCCHContainer ncch( + std::make_shared(sdmc_path, boot_content_path, "rb")); + if (ncch.Load() != ResultStatus::Success) { + LOG_WARNING(Core, "Could not load NCCH {}", boot_content_path); + out.push_back({type, id, FileUtil::Exists(citra_path + "content/"), + FileUtil::GetDirectoryTreeSize(directory + virtual_name + + "/content/")}); + break; + } + + const auto& [name, extdata_id, encryption, seed_crypto, icon] = + LoadTitleData(ncch); + out.push_back( + {type, id, FileUtil::Exists(citra_path + "content/"), + FileUtil::GetDirectoryTreeSize(directory + virtual_name + "/content/"), + name, extdata_id, encryption, seed_crypto, icon}); + } while (false); } if (type != ContentType::Application) { @@ -483,6 +545,96 @@ 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, + &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) { + if (!FileUtil::IsDirectory(directory + virtual_name + "/")) { + return true; + } + + if (!std::regex_match(virtual_name, title_regex)) { + return true; + } + + const u64 id = (high_id << 32) + std::stoull(virtual_name, nullptr, 16); + const auto citra_path = fmt::format( + "{}00000000000000000000000000000000/title/{:08x}/{}/", + FileUtil::GetUserPath(FileUtil::UserPath::NANDDir), high_id, virtual_name); + + const auto content_path = directory + virtual_name + "/content/"; + if (FileUtil::Exists(content_path)) { + do { + TitleMetadata tmd; + if (!LoadNandTMD(content_path, tmd)) { + LOG_WARNING(Core, "Could not load tmd from {}", content_path); + out.push_back({ContentType::SystemTitle, id, + FileUtil::Exists(citra_path + "content/"), + FileUtil::GetDirectoryTreeSize(content_path)}); + break; + } + + const auto boot_content_path = + fmt::format("{}{:08x}.app", content_path, tmd.GetBootContentID()); + NCCHContainer ncch( + std::make_shared(boot_content_path, "rb")); + if (ncch.Load() != ResultStatus::Success) { + LOG_WARNING(Core, "Could not load NCCH {}", boot_content_path); + out.push_back({ContentType::SystemTitle, id, + FileUtil::Exists(citra_path + "content/"), + FileUtil::GetDirectoryTreeSize(content_path)}); + break; + } + + const auto& [name, extdata_id, encryption, seed_crypto, icon] = + LoadTitleData(ncch); + out.push_back( + {ContentType::SystemTitle, id, + FileUtil::Exists(citra_path + "content/"), + FileUtil::GetDirectoryTreeSize(directory + virtual_name + "/content/"), + name, extdata_id, encryption, seed_crypto, icon}); + } while (false); + } + return true; + }); + }; + + ProcessDirectory(0x00040010); + ProcessDirectory(0x0004001b); + ProcessDirectory(0x00040030); + ProcessDirectory(0x000400db); + ProcessDirectory(0x00040130); +} + void SDMCImporter::ListExtdata(std::vector& out) const { FileUtil::ForeachDirectoryEntry( nullptr, fmt::format("{}extdata/00000000/", config.sdmc_path), @@ -627,6 +779,8 @@ void SDMCImporter::DeleteContent(const ContentSpecifier& specifier) { return DeleteSystemArchive(specifier.id); case ContentType::Sysdata: return DeleteSysdata(specifier.id); + case ContentType::SystemTitle: + return DeleteNandTitle(specifier.id); default: UNREACHABLE(); } @@ -640,6 +794,12 @@ void SDMCImporter::DeleteTitle(u64 id) const { FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir), (id >> 32), (id & 0xFFFFFFFF))); } +void SDMCImporter::DeleteNandTitle(u64 id) const { + FileUtil::DeleteDirRecursively(fmt::format( + "{}00000000000000000000000000000000/title/{:08x}/{:08x}/content/", + FileUtil::GetUserPath(FileUtil::UserPath::NANDDir), (id >> 32), (id & 0xFFFFFFFF))); +} + void SDMCImporter::DeleteSavegame(u64 id) const { FileUtil::DeleteDirRecursively(fmt::format( "{}Nintendo " @@ -721,6 +881,7 @@ std::vector LoadPresetConfig(std::string mount_point) { LOAD_DATA(secret_sector_path, SECRET_SECTOR); LOAD_DATA(config_savegame_path, "config.sav"); LOAD_DATA(system_archives_path, "sysarchives/"); + LOAD_DATA(system_titles_path, "title/"); #undef LOAD_DATA } diff --git a/src/core/importer.h b/src/core/importer.h index b80f638..d1f0fc4 100644 --- a/src/core/importer.h +++ b/src/core/importer.h @@ -26,6 +26,7 @@ enum class ContentType { Extdata, SystemArchive, Sysdata, + SystemTitle, }; /** @@ -77,8 +78,12 @@ struct Config { 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. }; +class SDMCFile; + +template class NCCHContainer; class SDMCImporter { @@ -140,37 +145,31 @@ private: bool Init(); bool ImportTitle(const ContentSpecifier& specifier, const ProgressCallback& callback); + bool ImportNandTitle(const ContentSpecifier& specifier, const ProgressCallback& callback); bool ImportSavegame(u64 id, const ProgressCallback& callback); bool ImportExtdata(u64 id, const ProgressCallback& callback); bool ImportSystemArchive(u64 id, const ProgressCallback& callback); bool ImportSysdata(u64 id, const ProgressCallback& callback); void ListTitle(std::vector& out) const; + void ListNandTitle(std::vector& out) const; void ListExtdata(std::vector& out) const; void ListSystemArchive(std::vector& out) const; void ListSysdata(std::vector& out) 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; - /** - * Loads the English short title name, extdata id, encryption and icon of a title. - * @param path Path of the 'content' folder relative to the SDMC root folder. - * Required to end with '/'. - * @return {name, extdata_id, encryption, seed_crypto, icon} - */ - std::tuple> LoadTitleData( - const std::string& path) const; - bool is_good{}; Config config; std::unique_ptr decryptor; // The NCCH used to dump CXIs. - std::unique_ptr dump_cxi_ncch; + std::unique_ptr> dump_cxi_ncch; }; /** diff --git a/src/core/ncch/ncch_container.cpp b/src/core/ncch/ncch_container.cpp index ab696e1..fa6c810 100644 --- a/src/core/ncch/ncch_container.cpp +++ b/src/core/ncch/ncch_container.cpp @@ -29,27 +29,24 @@ constexpr u32 MakeMagic(char a, char b, char c, char d) { 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(const std::string& root_folder, const std::string& filepath) - : root_folder(root_folder), filepath(filepath) { - file = std::make_shared(root_folder, filepath, "rb"); -} +template +NCCHContainer::NCCHContainer(std::shared_ptr file_) : file(std::move(file_)) {} -ResultStatus NCCHContainer::OpenFile(const std::string& root_folder, const std::string& filepath) { - this->root_folder = root_folder; - this->filepath = filepath; - file = std::make_shared(root_folder, filepath, "rb"); +template +ResultStatus NCCHContainer::OpenFile(std::shared_ptr file_) { + file = std::move(file_); if (!file->IsOpen()) { - LOG_WARNING(Service_FS, "Failed to open {}", filepath); + LOG_WARNING(Service_FS, "Failed to open"); return ResultStatus::Error; } - LOG_DEBUG(Service_FS, "Opened {}", filepath); + LOG_DEBUG(Service_FS, "Opened"); return ResultStatus::Success; } -ResultStatus NCCHContainer::Load() { - LOG_INFO(Service_FS, "Loading NCCH from file {}", filepath); +template +ResultStatus NCCHContainer::Load() { if (is_loaded) return ResultStatus::Success; @@ -188,7 +185,7 @@ ResultStatus NCCHContainer::Load() { // System archives and DLC don't have an extended header but have RomFS if (ncch_header.extended_header_size) { - auto read_exheader = [this](SDMCFile& file) { + auto read_exheader = [this](File& file) { const std::size_t size = sizeof(exheader_header); return file && file.ReadBytes(&exheader_header, size) == size; }; @@ -260,7 +257,7 @@ ResultStatus NCCHContainer::Load() { .ProcessData(data, data, sizeof(exefs_header)); } - exefs_file = std::make_shared(root_folder, filepath, "rb"); + exefs_file = file; has_exefs = true; } @@ -272,7 +269,8 @@ ResultStatus NCCHContainer::Load() { return ResultStatus::Success; } -ResultStatus NCCHContainer::LoadSectionExeFS(const char* name, std::vector& buffer) { +template +ResultStatus NCCHContainer::LoadSectionExeFS(const char* name, std::vector& buffer) { ResultStatus result = Load(); if (result != ResultStatus::Success) return result; @@ -309,7 +307,8 @@ ResultStatus NCCHContainer::LoadSectionExeFS(const char* name, std::vector& return ResultStatus::ErrorNotUsed; } -ResultStatus NCCHContainer::ReadProgramId(u64_le& program_id) { +template +ResultStatus NCCHContainer::ReadProgramId(u64_le& program_id) { ResultStatus result = Load(); if (result != ResultStatus::Success) return result; @@ -321,7 +320,8 @@ ResultStatus NCCHContainer::ReadProgramId(u64_le& program_id) { return ResultStatus::Success; } -ResultStatus NCCHContainer::ReadExtdataId(u64& extdata_id) { +template +ResultStatus NCCHContainer::ReadExtdataId(u64& extdata_id) { ResultStatus result = Load(); if (result != ResultStatus::Success) return result; @@ -356,7 +356,8 @@ ResultStatus NCCHContainer::ReadExtdataId(u64& extdata_id) { return ResultStatus::Success; } -bool NCCHContainer::HasExeFS() { +template +bool NCCHContainer::HasExeFS() { ResultStatus result = Load(); if (result != ResultStatus::Success) return false; @@ -364,7 +365,8 @@ bool NCCHContainer::HasExeFS() { return has_exefs; } -bool NCCHContainer::HasExHeader() { +template +bool NCCHContainer::HasExHeader() { ResultStatus result = Load(); if (result != ResultStatus::Success) return false; @@ -372,7 +374,8 @@ bool NCCHContainer::HasExHeader() { return has_exheader; } -ResultStatus NCCHContainer::ReadEncryptionType(EncryptionType& encryption) { +template +ResultStatus NCCHContainer::ReadEncryptionType(EncryptionType& encryption) { ResultStatus result = Load(); if (result != ResultStatus::Success) return result; @@ -407,7 +410,8 @@ ResultStatus NCCHContainer::ReadEncryptionType(EncryptionType& encryption) { return ResultStatus::Success; } -ResultStatus NCCHContainer::ReadSeedCrypto(bool& used) { +template +ResultStatus NCCHContainer::ReadSeedCrypto(bool& used) { ResultStatus result = Load(); if (result != ResultStatus::Success) return result; @@ -419,8 +423,9 @@ ResultStatus NCCHContainer::ReadSeedCrypto(bool& used) { return ResultStatus::Success; } -ResultStatus NCCHContainer::DecryptToFile(const std::string& destination, - const ProgressCallback& callback) { +template +ResultStatus NCCHContainer::DecryptToFile(const std::string& destination, + const ProgressCallback& callback) { ResultStatus result = Load(); if (result != ResultStatus::Success) return result; @@ -545,11 +550,15 @@ ResultStatus NCCHContainer::DecryptToFile(const std::string& destination, return ResultStatus::Success; } -void NCCHContainer::AbortDecryptToFile() { +template +void NCCHContainer::AbortDecryptToFile() { aborted = true; decryptor.Abort(); } +template class NCCHContainer; +template class NCCHContainer; + #pragma pack(push, 1) struct RomFSIVFCHeader { u32_le magic; diff --git a/src/core/ncch/ncch_container.h b/src/core/ncch/ncch_container.h index 8152852..47d9b4a 100644 --- a/src/core/ncch/ncch_container.h +++ b/src/core/ncch/ncch_container.h @@ -204,17 +204,13 @@ enum class EncryptionType; * Note that this is heavily stripped down and can only read (primary-key * encrypted non-code sections of) ExeFS and ExHeader by design. */ +template class NCCHContainer { public: - /** - * Constructs the container. - * @param root_folder Path to SDMC folder - * @param filepath Path relative to SDMC folder, starting with / - */ - NCCHContainer(const std::string& root_folder, const std::string& filepath); + NCCHContainer(std::shared_ptr file); NCCHContainer() {} - ResultStatus OpenFile(const std::string& root_folder, const std::string& filepath); + ResultStatus OpenFile(std::shared_ptr file); /** * Ensure ExeFS and exheader is loaded and ready for reading sections @@ -302,11 +298,11 @@ private: std::string root_folder; std::string filepath; - std::shared_ptr file; - std::shared_ptr exefs_file; + std::shared_ptr file; + std::shared_ptr exefs_file; // Used for DecryptToFile - QuickDecryptor decryptor; + QuickDecryptor decryptor; std::atomic_bool aborted{false}; }; diff --git a/src/frontend/import_dialog.cpp b/src/frontend/import_dialog.cpp index ff9491b..106656a 100644 --- a/src/frontend/import_dialog.cpp +++ b/src/frontend/import_dialog.cpp @@ -35,7 +35,7 @@ QString ReadableByteSize(qulonglong size) { } // content type, name, icon name -static constexpr std::array, 7> +static constexpr std::array, 8> ContentTypeMap{{ {Core::ContentType::Application, QT_TR_NOOP("Application"), "app"}, {Core::ContentType::Update, QT_TR_NOOP("Update"), "update"}, @@ -44,6 +44,7 @@ static constexpr std::array EncryptionTypeMap{{ @@ -206,7 +207,7 @@ void ImportDialog::InsertSecondLevelItem(std::size_t row, const Core::ContentSpe name = QStringLiteral("%1 (%2)") .arg(GetContentName(content)) .arg(GetContentTypeName(content.type)); - } else if (row <= 2) { + } else if (row <= 3) { name = GetContentName(content); } else { name = GetContentTypeName(content.type); @@ -225,7 +226,8 @@ void ImportDialog::InsertSecondLevelItem(std::size_t row, const Core::ContentSpe } if (content.type != Core::ContentType::Application && - content.type != Core::ContentType::Update && content.type != Core::ContentType::DLC) { + content.type != Core::ContentType::Update && content.type != Core::ContentType::DLC && + content.type != Core::ContentType::SystemTitle) { // Do not display encryption in this case encryption.clear(); @@ -237,11 +239,15 @@ void ImportDialog::InsertSecondLevelItem(std::size_t row, const Core::ContentSpe QPixmap icon; if (replace_icon.isNull()) { - // When not in title view, only 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 = use_title_view ? GetContentTypeIcon(content.type) - : GetContentIcon(content, use_category_icon); + // Exclude system titles, they are a single group but have own icons. + if (use_title_view && content.type != Core::ContentType::SystemTitle) { + 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; } @@ -258,13 +264,14 @@ void ImportDialog::InsertSecondLevelItem(std::size_t row, const Core::ContentSpe } else { if (!warning_shown && !exists && (type == Core::ContentType::SystemArchive || - type == Core::ContentType::Sysdata)) { + type == Core::ContentType::Sysdata || + type == Core::ContentType::SystemTitle)) { QMessageBox::warning( this, tr("Warning"), - tr("System Archive and System Data are 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.")); + 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.")); warning_shown = true; } total_size -= size; @@ -302,10 +309,12 @@ void ImportDialog::RepopulateContent() { title_name_map.insert_or_assign(0, tr("Ungrouped")); title_name_map.insert_or_assign(1, tr("System Archive")); title_name_map.insert_or_assign(2, tr("System Data")); + title_name_map.insert_or_assign(3, tr("System Title")); title_icon_map.insert_or_assign(0, QIcon::fromTheme(QStringLiteral("unknown")).pixmap(24)); title_icon_map.insert_or_assign(1, GetContentTypeIcon(Core::ContentType::SystemArchive)); title_icon_map.insert_or_assign(2, GetContentTypeIcon(Core::ContentType::Sysdata)); + title_icon_map.insert_or_assign(3, GetContentTypeIcon(Core::ContentType::SystemTitle)); std::unordered_map title_row_map; for (const auto& [id, name] : title_name_map) { @@ -342,6 +351,10 @@ void ImportDialog::RepopulateContent() { row = title_row_map.at(2); // System data break; } + case Core::ContentType::SystemTitle: { + row = title_row_map.at(3); // System title + break; + } } InsertSecondLevelItem(row, content, i); diff --git a/src/frontend/import_dialog.h b/src/frontend/import_dialog.h index cdcfcdc..d895a01 100644 --- a/src/frontend/import_dialog.h +++ b/src/frontend/import_dialog.h @@ -42,8 +42,8 @@ private: Core::ContentSpecifier SpecifierFromItem(QTreeWidgetItem* item) const; void OnContextMenu(const QPoint& point); void StartDumpingCXI(const Core::ContentSpecifier& content); - Core::NCCHContainer dump_cxi_container; // NCCH container used for dumping CXI - QString last_dump_cxi_path; // Used for recording last path in StartDumpingCXI + Core::NCCHContainer dump_cxi_container; // NCCH container used for dumping CXI + QString last_dump_cxi_path; // Used for recording last path in StartDumpingCXI std::unique_ptr ui;