diff --git a/src/core/file_sys/ncch_container.cpp b/src/core/file_sys/ncch_container.cpp index 273474c..85ef819 100644 --- a/src/core/file_sys/ncch_container.cpp +++ b/src/core/file_sys/ncch_container.cpp @@ -42,218 +42,217 @@ bool NCCHContainer::Load() { if (is_loaded) return true; - if (file->IsOpen()) { - // Reset read pointer in case this file has been read before. - file->Seek(0, SEEK_SET); + if (!file->IsOpen()) { + LOG_WARNING(Service_FS, "Failed to open"); + return false; + } - if (file->ReadBytes(&ncch_header, sizeof(NCCH_Header)) != sizeof(NCCH_Header)) { - LOG_ERROR(Service_FS, "Could not read from file"); - return false; - } + // Reset read pointer in case this file has been read before. + file->Seek(0, SEEK_SET); - // 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; - } + if (file->ReadBytes(&ncch_header, sizeof(NCCH_Header)) != sizeof(NCCH_Header)) { + LOG_ERROR(Service_FS, "Could not read from file"); + return false; + } - bool failed_to_decrypt = false; - if (!ncch_header.no_crypto) { - is_encrypted = true; + // 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; + } - // 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); + 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 { - 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"); + 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; - } - 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; + } 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()); } } - // 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); + Key::SetKeyY(Key::NCCHSecure1, key_y_primary); + if (!Key::IsNormalKeyAvailable(Key::NCCHSecure1)) { + LOG_ERROR(Service_FS, "Secure1 KeyX missing"); failed_to_decrypt = true; } - } else { - LOG_DEBUG(Service_FS, "No crypto"); - is_encrypted = false; - } + primary_key = Key::GetNormalKey(Key::NCCHSecure1); - // 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)); + 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; } - - 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; + // 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; } - - if (ncch_header.romfs_offset != 0 && ncch_header.romfs_size != 0) - has_romfs = 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; } diff --git a/src/core/importer.cpp b/src/core/importer.cpp index c6e9919..db124f9 100644 --- a/src/core/importer.cpp +++ b/src/core/importer.cpp @@ -535,10 +535,29 @@ bool SDMCImporter::LoadTMD(const ContentSpecifier& specifier, TitleMetadata& out return LoadTMD(specifier.type, specifier.id, out); } +std::shared_ptr SDMCImporter::OpenContent(const ContentSpecifier& specifier, + u32 content_id) const { + if (IsNandTitle(specifier.type)) { + const auto path = + fmt::format("{}{:08x}/{:08x}/content/{:08x}.app", config.system_titles_path, + (specifier.id >> 32), (specifier.id & 0xFFFFFFFF), content_id); + return std::make_shared(path, "rb"); + } else { + // For DLCs, there one subfolder every 256 titles, but in practice hardcoded 00000000 + // should be fine (also matches GodMode9 behaviour) + const auto format_str = specifier.type == ContentType::DLC + ? "/title/{:08x}/{:08x}/content/00000000/{:08x}.app" + : "/title/{:08x}/{:08x}/content/{:08x}.app"; + const auto path = + fmt::format(format_str, (specifier.id >> 32), (specifier.id & 0xFFFFFFFF), content_id); + return std::make_shared(config.sdmc_path, path, "rb"); + } +} + // English short title name, extdata id, icon using TitleData = std::tuple>; -TitleData LoadTitleData(NCCHContainer& ncch) { +static TitleData LoadTitleData(NCCHContainer& ncch) { std::string codeset_name; ncch.ReadCodesetName(codeset_name); @@ -634,11 +653,7 @@ bool SDMCImporter::DumpCXI(const ContentSpecifier& specifier, std::string destin return false; } - const auto boot_content_path = - 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")); + dump_cxi_ncch = std::make_unique(OpenContent(specifier, tmd.GetBootContentID())); if (destination.back() == '/' || destination.back() == '\\') { auto_filename = true; @@ -703,13 +718,6 @@ bool SDMCImporter::BuildCIA(CIABuildType build_type, const ContentSpecifier& spe return false; } - const bool is_nand = IsNandTitle(specifier.type); - 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)); - if (destination.back() == '/' || destination.back() == '\\') { auto_filename = true; } @@ -722,15 +730,12 @@ bool SDMCImporter::BuildCIA(CIABuildType build_type, const ContentSpecifier& spe if (destination.back() != '/' && destination.back() != '\\') { destination.push_back('/'); } - const auto boot_content_path = - fmt::format("{}{:08x}.app", physical_path, tmd.GetBootContentID()); - NCCHContainer ncch; - if (is_nand) { - ncch.OpenFile(std::make_shared(boot_content_path, "rb")); - } else { - const auto relative_path = boot_content_path.substr(config.sdmc_path.size() - 1); - ncch.OpenFile(std::make_shared(config.sdmc_path, relative_path, "rb")); + auto file = OpenContent(specifier, tmd.GetBootContentID()); + if (!file) { + LOG_ERROR(Core, "Could not open boot content"); + return false; } + NCCHContainer ncch(std::move(file)); const auto filename = fmt::format("{} (v{}).{}", GetTitleFileName(ncch), tmd.GetTitleVersionString(), BuildTypeExts.at(static_cast(build_type))); @@ -749,28 +754,18 @@ bool SDMCImporter::BuildCIA(CIABuildType build_type, const ContentSpecifier& spe } for (const auto& tmd_chunk : tmd.tmd_chunks) { - // For DLCs, there one subfolder every 256 titles, but in practice hardcoded 00000000 - // should be fine (also matches GodMode9) - const auto sub_folder = - specifier.type == ContentType::DLC ? physical_path + "00000000/" : physical_path; - const auto path = fmt::format("{}{:08x}.app", sub_folder, static_cast(tmd_chunk.id)); - if (!FileUtil::Exists(path)) { + auto file = OpenContent(specifier, tmd_chunk.id); + if (!file) { if (static_cast(tmd_chunk.type) & 0x4000) { // optional continue; } - LOG_ERROR(Core, "Content {:08x} does not exist", static_cast(tmd_chunk.id)); + LOG_ERROR(Core, "Could not open content {:08x}", static_cast(tmd_chunk.id)); ret = false; return false; } - if (is_nand) { - NCCHContainer ncch(std::make_shared(path, "rb")); - ret = cia_builder->AddContent(tmd_chunk.id, ncch); - } else { - const auto relative_path = path.substr(config.sdmc_path.size() - 1); - NCCHContainer ncch(std::make_shared(config.sdmc_path, relative_path, "rb")); - ret = cia_builder->AddContent(tmd_chunk.id, ncch); - } + NCCHContainer ncch(std::move(file)); + ret = cia_builder->AddContent(tmd_chunk.id, ncch); if (!ret) { return false; } @@ -821,35 +816,18 @@ bool SDMCImporter::CheckTitleContents(const ContentSpecifier& specifier, Common::ProgressCallbackWrapper wrapper{specifier.maximum_size}; - const bool is_nand = IsNandTitle(specifier.type); - 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)); for (const auto& tmd_chunk : tmd.tmd_chunks) { - // For DLCs, there one subfolder every 256 titles, but in practice hardcoded 00000000 - // should be fine (also matches GodMode9) - const auto sub_folder = - specifier.type == ContentType::DLC ? physical_path + "00000000/" : physical_path; - const auto path = fmt::format("{}{:08x}.app", sub_folder, static_cast(tmd_chunk.id)); - if (!FileUtil::Exists(path)) { + auto file = OpenContent(specifier, tmd_chunk.id); + if (!file) { if (static_cast(tmd_chunk.type) & 0x4000) { // optional continue; } - LOG_INFO(Core, "Content {:08x} does not exist", static_cast(tmd_chunk.id)); + LOG_INFO(Core, "Could not open content {:08x}", static_cast(tmd_chunk.id)); return false; } - std::shared_ptr source_file; - if (is_nand) { - source_file = std::make_shared(path, "rb"); - } else { - const auto relative_path = path.substr(config.sdmc_path.size() - 1); - source_file = std::make_shared(config.sdmc_path, relative_path, "rb"); - } std::shared_ptr dest_file = std::make_shared(); - if (!file_decryptor.CryptAndWriteFile(source_file, source_file->GetSize(), dest_file, + if (!file_decryptor.CryptAndWriteFile(file, file->GetSize(), dest_file, wrapper.Wrap(callback))) { return false; } diff --git a/src/core/importer.h b/src/core/importer.h index fb5f47d..64d7109 100644 --- a/src/core/importer.h +++ b/src/core/importer.h @@ -176,8 +176,8 @@ public: bool LoadTMD(const ContentSpecifier& specifier, TitleMetadata& out) const; std::string GetTitleContentsPath(const ContentSpecifier& specifier) const; - std::shared_ptr OpenBootContent(const ContentSpecifier& specifier, - const TitleMetadata& tmd) const; + std::shared_ptr OpenContent(const ContentSpecifier& specifier, + u32 content_id) const; std::shared_ptr& GetTicketDB() { return ticket_db; diff --git a/src/frontend/import_dialog.cpp b/src/frontend/import_dialog.cpp index 77a4bab..1eabb26 100644 --- a/src/frontend/import_dialog.cpp +++ b/src/frontend/import_dialog.cpp @@ -490,7 +490,7 @@ void ImportDialog::OnContextMenu(const QPoint& point) { [this, specifier] { StartBuildingCIASingle(specifier); }); QAction* show_title_info = context_menu.addAction(tr("Show Title Info")); connect(show_title_info, &QAction::triggered, [this, specifier] { - TitleInfoDialog dialog(this, config, *importer, specifier); + TitleInfoDialog dialog(this, *importer, specifier); dialog.exec(); }); } diff --git a/src/frontend/title_info_dialog.cpp b/src/frontend/title_info_dialog.cpp index c830c35..26ca74e 100644 --- a/src/frontend/title_info_dialog.cpp +++ b/src/frontend/title_info_dialog.cpp @@ -15,8 +15,8 @@ #include "frontend/title_info_dialog.h" #include "ui_title_info_dialog.h" -TitleInfoDialog::TitleInfoDialog(QWidget* parent, const Core::Config& config, - Core::SDMCImporter& importer_, Core::ContentSpecifier specifier_) +TitleInfoDialog::TitleInfoDialog(QWidget* parent, Core::SDMCImporter& importer_, + Core::ContentSpecifier specifier_) : QDialog(parent), ui(std::make_unique()), importer(importer_), specifier(std::move(specifier_)) { @@ -25,39 +25,62 @@ TitleInfoDialog::TitleInfoDialog(QWidget* parent, const Core::Config& config, const double scale = qApp->desktop()->logicalDpiX() / 96.0; resize(static_cast(width() * scale), static_cast(height() * scale)); - LoadInfo(config); + LoadInfo(); connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &TitleInfoDialog::accept); } TitleInfoDialog::~TitleInfoDialog() = default; -void TitleInfoDialog::LoadInfo(const Core::Config& config) { +void TitleInfoDialog::LoadInfo() { + // Load TMD & boot NCCH Core::TitleMetadata tmd; - if (!importer.LoadTMD(specifier, 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; } - ui->versionLineEdit->setText(QString::fromStdString(tmd.GetTitleVersionString())); - ui->titleIDLineEdit->setText(QStringLiteral("%1").arg(specifier.id, 16, 16, QLatin1Char{'0'})); - const bool is_nand = Core::IsNandTitle(specifier.type); - 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)); - const auto boot_content_path = - fmt::format("{}{:08x}.app", physical_path, tmd.GetBootContentID()); - Core::NCCHContainer ncch; - if (is_nand) { - ncch.OpenFile(std::make_shared(boot_content_path, "rb")); - } else { - const auto relative_path = boot_content_path.substr(config.sdmc_path.size() - 1); - ncch.OpenFile(std::make_shared(config.sdmc_path, relative_path, "rb")); + // 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) { + ui->iconLargeLabel->setPixmap( + QPixmap::fromImage(QImage(reinterpret_cast(smdh.GetIcon(true).data()), 48, + 48, QImage::Format::Format_RGB16))); + ui->iconSmallLabel->setPixmap( + QPixmap::fromImage(QImage(reinterpret_cast(smdh.GetIcon(false).data()), + 24, 24, QImage::Format::Format_RGB16))); + } + + // 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")}, @@ -78,50 +101,6 @@ void TitleInfoDialog::LoadInfo(const Core::Config& config) { encryption_text.append(tr(" (Seed)")); } ui->encryptionLineEdit->setText(encryption_text); - - // Checks - 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); - - // Load SMDH - std::vector smdh_buffer; - if (!ncch.LoadSectionExeFS("icon", smdh_buffer) || smdh_buffer.size() != sizeof(Core::SMDH) || - !Core::IsValidSMDH(smdh_buffer)) { - - LOG_WARNING(Core, "Failed to load SMDH"); - ui->namesGroupBox->setEnabled(false); - return; - } - - std::memcpy(&smdh, smdh_buffer.data(), smdh_buffer.size()); - // Load icon - ui->iconLargeLabel->setPixmap( - QPixmap::fromImage(QImage(reinterpret_cast(smdh.GetIcon(true).data()), 48, 48, - QImage::Format::Format_RGB16))); - ui->iconSmallLabel->setPixmap( - QPixmap::fromImage(QImage(reinterpret_cast(smdh.GetIcon(false).data()), 24, - 24, QImage::Format::Format_RGB16))); - // Load names - InitializeLanguageComboBox(); } void TitleInfoDialog::InitializeLanguageComboBox() { @@ -172,6 +151,30 @@ void TitleInfoDialog::UpdateNames() { 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, diff --git a/src/frontend/title_info_dialog.h b/src/frontend/title_info_dialog.h index 1803b10..ccc5a39 100644 --- a/src/frontend/title_info_dialog.h +++ b/src/frontend/title_info_dialog.h @@ -9,6 +9,7 @@ namespace Core { struct Config; class ContentSpecifier; +class NCCHContainer; class SDMCImporter; class TitleMetadata; } // namespace Core @@ -21,13 +22,17 @@ class TitleInfoDialog : public QDialog { Q_OBJECT public: - explicit TitleInfoDialog(QWidget* parent, const Core::Config& config, - Core::SDMCImporter& importer, Core::ContentSpecifier specifier); + explicit TitleInfoDialog(QWidget* parent, Core::SDMCImporter& importer, + Core::ContentSpecifier specifier); ~TitleInfoDialog(); private: - void LoadInfo(const Core::Config& config); + void LoadInfo(); + + void LoadEncryption(Core::NCCHContainer& ncch); void InitializeLanguageComboBox(); + void InitializeChecks(Core::TitleMetadata& tmd); + void UpdateNames(); void ExecuteContentsCheck();