diff options
Diffstat (limited to '')
-rw-r--r-- | src/core/file_sys/card_image.cpp | 6 | ||||
-rw-r--r-- | src/core/file_sys/content_archive.cpp | 179 | ||||
-rw-r--r-- | src/core/file_sys/content_archive.h | 11 | ||||
-rw-r--r-- | src/core/file_sys/nca_patch.cpp | 206 | ||||
-rw-r--r-- | src/core/file_sys/nca_patch.h | 147 | ||||
-rw-r--r-- | src/core/file_sys/patch_manager.cpp | 153 | ||||
-rw-r--r-- | src/core/file_sys/patch_manager.h | 62 | ||||
-rw-r--r-- | src/core/file_sys/registered_cache.cpp | 115 | ||||
-rw-r--r-- | src/core/file_sys/registered_cache.h | 40 | ||||
-rw-r--r-- | src/core/file_sys/romfs_factory.cpp | 13 | ||||
-rw-r--r-- | src/core/file_sys/romfs_factory.h | 2 | ||||
-rw-r--r-- | src/core/file_sys/submission_package.cpp | 17 | ||||
-rw-r--r-- | src/core/file_sys/submission_package.h | 13 |
13 files changed, 932 insertions, 32 deletions
diff --git a/src/core/file_sys/card_image.cpp b/src/core/file_sys/card_image.cpp index 1bd3353e4..8218893b2 100644 --- a/src/core/file_sys/card_image.cpp +++ b/src/core/file_sys/card_image.cpp @@ -52,11 +52,11 @@ XCI::XCI(VirtualFile file_) : file(std::move(file_)), partitions(0x4) { const auto secure_ncas = secure_partition->GetNCAsCollapsed(); std::copy(secure_ncas.begin(), secure_ncas.end(), std::back_inserter(ncas)); - program_nca_status = Loader::ResultStatus::ErrorXCIMissingProgramNCA; program = secure_partition->GetNCA(secure_partition->GetProgramTitleID(), ContentRecordType::Program); - if (program != nullptr) - program_nca_status = program->GetStatus(); + program_nca_status = secure_partition->GetProgramStatus(secure_partition->GetProgramTitleID()); + if (program_nca_status == Loader::ResultStatus::ErrorNSPMissingProgramNCA) + program_nca_status = Loader::ResultStatus::ErrorXCIMissingProgramNCA; auto result = AddNCAFromPartition(XCIPartition::Update); if (result != Loader::ResultStatus::Success) { diff --git a/src/core/file_sys/content_archive.cpp b/src/core/file_sys/content_archive.cpp index 7cfb6f36b..79bfb6fec 100644 --- a/src/core/file_sys/content_archive.cpp +++ b/src/core/file_sys/content_archive.cpp @@ -12,6 +12,7 @@ #include "core/crypto/aes_util.h" #include "core/crypto/ctr_encryption_layer.h" #include "core/file_sys/content_archive.h" +#include "core/file_sys/nca_patch.h" #include "core/file_sys/partition_filesystem.h" #include "core/file_sys/romfs.h" #include "core/file_sys/vfs_offset.h" @@ -68,10 +69,31 @@ struct RomFSSuperblock { }; static_assert(sizeof(RomFSSuperblock) == 0x200, "RomFSSuperblock has incorrect size."); +struct BKTRHeader { + u64_le offset; + u64_le size; + u32_le magic; + INSERT_PADDING_BYTES(0x4); + u32_le number_entries; + INSERT_PADDING_BYTES(0x4); +}; +static_assert(sizeof(BKTRHeader) == 0x20, "BKTRHeader has incorrect size."); + +struct BKTRSuperblock { + NCASectionHeaderBlock header_block; + IVFCHeader ivfc; + INSERT_PADDING_BYTES(0x18); + BKTRHeader relocation; + BKTRHeader subsection; + INSERT_PADDING_BYTES(0xC0); +}; +static_assert(sizeof(BKTRSuperblock) == 0x200, "BKTRSuperblock has incorrect size."); + union NCASectionHeader { NCASectionRaw raw; PFS0Superblock pfs0; RomFSSuperblock romfs; + BKTRSuperblock bktr; }; static_assert(sizeof(NCASectionHeader) == 0x200, "NCASectionHeader has incorrect size."); @@ -104,7 +126,7 @@ boost::optional<Core::Crypto::Key128> NCA::GetKeyAreaKey(NCASectionCryptoType ty Core::Crypto::Key128 out; if (type == NCASectionCryptoType::XTS) std::copy(key_area.begin(), key_area.begin() + 0x10, out.begin()); - else if (type == NCASectionCryptoType::CTR) + else if (type == NCASectionCryptoType::CTR || type == NCASectionCryptoType::BKTR) std::copy(key_area.begin() + 0x20, key_area.begin() + 0x30, out.begin()); else LOG_CRITICAL(Crypto, "Called GetKeyAreaKey on invalid NCASectionCryptoType type={:02X}", @@ -154,6 +176,9 @@ VirtualFile NCA::Decrypt(NCASectionHeader s_header, VirtualFile in, u64 starting LOG_DEBUG(Crypto, "called with mode=NONE"); return in; case NCASectionCryptoType::CTR: + // During normal BKTR decryption, this entire function is skipped. This is for the metadata, + // which uses the same CTR as usual. + case NCASectionCryptoType::BKTR: LOG_DEBUG(Crypto, "called with mode=CTR, starting_offset={:016X}", starting_offset); { boost::optional<Core::Crypto::Key128> key = boost::none; @@ -190,7 +215,9 @@ VirtualFile NCA::Decrypt(NCASectionHeader s_header, VirtualFile in, u64 starting } } -NCA::NCA(VirtualFile file_) : file(std::move(file_)) { +NCA::NCA(VirtualFile file_, VirtualFile bktr_base_romfs_, u64 bktr_base_ivfc_offset) + : file(std::move(file_)), + bktr_base_romfs(bktr_base_romfs_ ? std::move(bktr_base_romfs_) : nullptr) { status = Loader::ResultStatus::Success; if (file == nullptr) { @@ -265,22 +292,21 @@ NCA::NCA(VirtualFile file_) : file(std::move(file_)) { is_update = std::find_if(sections.begin(), sections.end(), [](const NCASectionHeader& header) { return header.raw.header.crypto_type == NCASectionCryptoType::BKTR; }) != sections.end(); + ivfc_offset = 0; for (std::ptrdiff_t i = 0; i < number_sections; ++i) { auto section = sections[i]; if (section.raw.header.filesystem_type == NCASectionFilesystemType::ROMFS) { - const size_t romfs_offset = - header.section_tables[i].media_offset * MEDIA_OFFSET_MULTIPLIER + - section.romfs.ivfc.levels[IVFC_MAX_LEVEL - 1].offset; + const size_t base_offset = + header.section_tables[i].media_offset * MEDIA_OFFSET_MULTIPLIER; + ivfc_offset = section.romfs.ivfc.levels[IVFC_MAX_LEVEL - 1].offset; + const size_t romfs_offset = base_offset + ivfc_offset; const size_t romfs_size = section.romfs.ivfc.levels[IVFC_MAX_LEVEL - 1].size; - auto dec = - Decrypt(section, std::make_shared<OffsetVfsFile>(file, romfs_size, romfs_offset), - romfs_offset); - if (dec != nullptr) { - files.push_back(std::move(dec)); - romfs = files.back(); - } else { + auto raw = std::make_shared<OffsetVfsFile>(file, romfs_size, romfs_offset); + auto dec = Decrypt(section, raw, romfs_offset); + + if (dec == nullptr) { if (status != Loader::ResultStatus::Success) return; if (has_rights_id) @@ -289,6 +315,117 @@ NCA::NCA(VirtualFile file_) : file(std::move(file_)) { status = Loader::ResultStatus::ErrorIncorrectKeyAreaKey; return; } + + if (section.raw.header.crypto_type == NCASectionCryptoType::BKTR) { + if (section.bktr.relocation.magic != Common::MakeMagic('B', 'K', 'T', 'R') || + section.bktr.subsection.magic != Common::MakeMagic('B', 'K', 'T', 'R')) { + status = Loader::ResultStatus::ErrorBadBKTRHeader; + return; + } + + if (section.bktr.relocation.offset + section.bktr.relocation.size != + section.bktr.subsection.offset) { + status = Loader::ResultStatus::ErrorBKTRSubsectionNotAfterRelocation; + return; + } + + const u64 size = + MEDIA_OFFSET_MULTIPLIER * (header.section_tables[i].media_end_offset - + header.section_tables[i].media_offset); + if (section.bktr.subsection.offset + section.bktr.subsection.size != size) { + status = Loader::ResultStatus::ErrorBKTRSubsectionNotAtEnd; + return; + } + + const u64 offset = section.romfs.ivfc.levels[IVFC_MAX_LEVEL - 1].offset; + RelocationBlock relocation_block{}; + if (dec->ReadObject(&relocation_block, section.bktr.relocation.offset - offset) != + sizeof(RelocationBlock)) { + status = Loader::ResultStatus::ErrorBadRelocationBlock; + return; + } + SubsectionBlock subsection_block{}; + if (dec->ReadObject(&subsection_block, section.bktr.subsection.offset - offset) != + sizeof(RelocationBlock)) { + status = Loader::ResultStatus::ErrorBadSubsectionBlock; + return; + } + + std::vector<RelocationBucketRaw> relocation_buckets_raw( + (section.bktr.relocation.size - sizeof(RelocationBlock)) / + sizeof(RelocationBucketRaw)); + if (dec->ReadBytes(relocation_buckets_raw.data(), + section.bktr.relocation.size - sizeof(RelocationBlock), + section.bktr.relocation.offset + sizeof(RelocationBlock) - + offset) != + section.bktr.relocation.size - sizeof(RelocationBlock)) { + status = Loader::ResultStatus::ErrorBadRelocationBuckets; + return; + } + + std::vector<SubsectionBucketRaw> subsection_buckets_raw( + (section.bktr.subsection.size - sizeof(SubsectionBlock)) / + sizeof(SubsectionBucketRaw)); + if (dec->ReadBytes(subsection_buckets_raw.data(), + section.bktr.subsection.size - sizeof(SubsectionBlock), + section.bktr.subsection.offset + sizeof(SubsectionBlock) - + offset) != + section.bktr.subsection.size - sizeof(SubsectionBlock)) { + status = Loader::ResultStatus::ErrorBadSubsectionBuckets; + return; + } + + std::vector<RelocationBucket> relocation_buckets(relocation_buckets_raw.size()); + std::transform(relocation_buckets_raw.begin(), relocation_buckets_raw.end(), + relocation_buckets.begin(), &ConvertRelocationBucketRaw); + std::vector<SubsectionBucket> subsection_buckets(subsection_buckets_raw.size()); + std::transform(subsection_buckets_raw.begin(), subsection_buckets_raw.end(), + subsection_buckets.begin(), &ConvertSubsectionBucketRaw); + + u32 ctr_low; + std::memcpy(&ctr_low, section.raw.section_ctr.data(), sizeof(ctr_low)); + subsection_buckets.back().entries.push_back( + {section.bktr.relocation.offset, {0}, ctr_low}); + subsection_buckets.back().entries.push_back({size, {0}, 0}); + + boost::optional<Core::Crypto::Key128> key = boost::none; + if (encrypted) { + if (has_rights_id) { + status = Loader::ResultStatus::Success; + key = GetTitlekey(); + if (key == boost::none) { + status = Loader::ResultStatus::ErrorMissingTitlekey; + return; + } + } else { + key = GetKeyAreaKey(NCASectionCryptoType::BKTR); + if (key == boost::none) { + status = Loader::ResultStatus::ErrorMissingKeyAreaKey; + return; + } + } + } + + if (bktr_base_romfs == nullptr) { + status = Loader::ResultStatus::ErrorMissingBKTRBaseRomFS; + return; + } + + auto bktr = std::make_shared<BKTR>( + bktr_base_romfs, std::make_shared<OffsetVfsFile>(file, romfs_size, base_offset), + relocation_block, relocation_buckets, subsection_block, subsection_buckets, + encrypted, encrypted ? key.get() : Core::Crypto::Key128{}, base_offset, + bktr_base_ivfc_offset, section.raw.section_ctr); + + // BKTR applies to entire IVFC, so make an offset version to level 6 + + files.push_back(std::make_shared<OffsetVfsFile>( + bktr, romfs_size, section.romfs.ivfc.levels[IVFC_MAX_LEVEL - 1].offset)); + romfs = files.back(); + } else { + files.push_back(std::move(dec)); + romfs = files.back(); + } } else if (section.raw.header.filesystem_type == NCASectionFilesystemType::PFS0) { u64 offset = (static_cast<u64>(header.section_tables[i].media_offset) * MEDIA_OFFSET_MULTIPLIER) + @@ -304,6 +441,12 @@ NCA::NCA(VirtualFile file_) : file(std::move(file_)) { dirs.push_back(std::move(npfs)); if (IsDirectoryExeFS(dirs.back())) exefs = dirs.back(); + } else { + if (has_rights_id) + status = Loader::ResultStatus::ErrorIncorrectTitlekeyOrTitlekek; + else + status = Loader::ResultStatus::ErrorIncorrectKeyAreaKey; + return; } } else { if (status != Loader::ResultStatus::Success) @@ -349,11 +492,15 @@ NCAContentType NCA::GetType() const { } u64 NCA::GetTitleId() const { - if (status != Loader::ResultStatus::Success) - return {}; + if (is_update || status == Loader::ResultStatus::ErrorMissingBKTRBaseRomFS) + return header.title_id | 0x800; return header.title_id; } +bool NCA::IsUpdate() const { + return is_update; +} + VirtualFile NCA::GetRomFS() const { return romfs; } @@ -366,8 +513,8 @@ VirtualFile NCA::GetBaseFile() const { return file; } -bool NCA::IsUpdate() const { - return is_update; +u64 NCA::GetBaseIVFCOffset() const { + return ivfc_offset; } bool NCA::ReplaceFileWithSubdirectory(VirtualFile file, VirtualDir dir) { diff --git a/src/core/file_sys/content_archive.h b/src/core/file_sys/content_archive.h index 0ea666cac..00eca52da 100644 --- a/src/core/file_sys/content_archive.h +++ b/src/core/file_sys/content_archive.h @@ -79,7 +79,8 @@ bool IsValidNCA(const NCAHeader& header); // After construction, use GetStatus to determine if the file is valid and ready to be used. class NCA : public ReadOnlyVfsDirectory { public: - explicit NCA(VirtualFile file); + explicit NCA(VirtualFile file, VirtualFile bktr_base_romfs = nullptr, + u64 bktr_base_ivfc_offset = 0); Loader::ResultStatus GetStatus() const; std::vector<std::shared_ptr<VfsFile>> GetFiles() const override; @@ -89,13 +90,15 @@ public: NCAContentType GetType() const; u64 GetTitleId() const; + bool IsUpdate() const; VirtualFile GetRomFS() const; VirtualDir GetExeFS() const; VirtualFile GetBaseFile() const; - bool IsUpdate() const; + // Returns the base ivfc offset used in BKTR patching. + u64 GetBaseIVFCOffset() const; protected: bool ReplaceFileWithSubdirectory(VirtualFile file, VirtualDir dir) override; @@ -112,14 +115,16 @@ private: VirtualFile romfs = nullptr; VirtualDir exefs = nullptr; VirtualFile file; + VirtualFile bktr_base_romfs; + u64 ivfc_offset; NCAHeader header{}; bool has_rights_id{}; - bool is_update{}; Loader::ResultStatus status{}; bool encrypted; + bool is_update; Core::Crypto::KeyManager keys; }; diff --git a/src/core/file_sys/nca_patch.cpp b/src/core/file_sys/nca_patch.cpp new file mode 100644 index 000000000..e0111bffc --- /dev/null +++ b/src/core/file_sys/nca_patch.cpp @@ -0,0 +1,206 @@ +// Copyright 2018 yuzu emulator team +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "common/assert.h" +#include "core/crypto/aes_util.h" +#include "core/file_sys/nca_patch.h" + +namespace FileSys { + +BKTR::BKTR(VirtualFile base_romfs_, VirtualFile bktr_romfs_, RelocationBlock relocation_, + std::vector<RelocationBucket> relocation_buckets_, SubsectionBlock subsection_, + std::vector<SubsectionBucket> subsection_buckets_, bool is_encrypted_, + Core::Crypto::Key128 key_, u64 base_offset_, u64 ivfc_offset_, + std::array<u8, 8> section_ctr_) + : base_romfs(std::move(base_romfs_)), bktr_romfs(std::move(bktr_romfs_)), + relocation(relocation_), relocation_buckets(std::move(relocation_buckets_)), + subsection(subsection_), subsection_buckets(std::move(subsection_buckets_)), + encrypted(is_encrypted_), key(key_), base_offset(base_offset_), ivfc_offset(ivfc_offset_), + section_ctr(section_ctr_) { + for (size_t i = 0; i < relocation.number_buckets - 1; ++i) { + relocation_buckets[i].entries.push_back({relocation.base_offsets[i + 1], 0, 0}); + } + + for (size_t i = 0; i < subsection.number_buckets - 1; ++i) { + subsection_buckets[i].entries.push_back({subsection_buckets[i + 1].entries[0].address_patch, + {0}, + subsection_buckets[i + 1].entries[0].ctr}); + } + + relocation_buckets.back().entries.push_back({relocation.size, 0, 0}); +} + +BKTR::~BKTR() = default; + +size_t BKTR::Read(u8* data, size_t length, size_t offset) const { + // Read out of bounds. + if (offset >= relocation.size) + return 0; + const auto relocation = GetRelocationEntry(offset); + const auto section_offset = offset - relocation.address_patch + relocation.address_source; + const auto bktr_read = relocation.from_patch; + + const auto next_relocation = GetNextRelocationEntry(offset); + + if (offset + length > next_relocation.address_patch) { + const u64 partition = next_relocation.address_patch - offset; + return Read(data, partition, offset) + + Read(data + partition, length - partition, offset + partition); + } + + if (!bktr_read) { + ASSERT_MSG(section_offset >= ivfc_offset, "Offset calculation negative."); + return base_romfs->Read(data, length, section_offset - ivfc_offset); + } + + if (!encrypted) { + return bktr_romfs->Read(data, length, section_offset); + } + + const auto subsection = GetSubsectionEntry(section_offset); + Core::Crypto::AESCipher<Core::Crypto::Key128> cipher(key, Core::Crypto::Mode::CTR); + + // Calculate AES IV + std::vector<u8> iv(16); + auto subsection_ctr = subsection.ctr; + auto offset_iv = section_offset + base_offset; + for (size_t i = 0; i < section_ctr.size(); ++i) + iv[i] = section_ctr[0x8 - i - 1]; + offset_iv >>= 4; + for (size_t i = 0; i < sizeof(u64); ++i) { + iv[0xF - i] = static_cast<u8>(offset_iv & 0xFF); + offset_iv >>= 8; + } + for (size_t i = 0; i < sizeof(u32); ++i) { + iv[0x7 - i] = static_cast<u8>(subsection_ctr & 0xFF); + subsection_ctr >>= 8; + } + cipher.SetIV(iv); + + const auto next_subsection = GetNextSubsectionEntry(section_offset); + + if (section_offset + length > next_subsection.address_patch) { + const u64 partition = next_subsection.address_patch - section_offset; + return Read(data, partition, offset) + + Read(data + partition, length - partition, offset + partition); + } + + const auto block_offset = section_offset & 0xF; + if (block_offset != 0) { + auto block = bktr_romfs->ReadBytes(0x10, section_offset & ~0xF); + cipher.Transcode(block.data(), block.size(), block.data(), Core::Crypto::Op::Decrypt); + if (length + block_offset < 0x10) { + std::memcpy(data, block.data() + block_offset, std::min(length, block.size())); + return std::min(length, block.size()); + } + + const auto read = 0x10 - block_offset; + std::memcpy(data, block.data() + block_offset, read); + return read + Read(data + read, length - read, offset + read); + } + + const auto raw_read = bktr_romfs->Read(data, length, section_offset); + cipher.Transcode(data, raw_read, data, Core::Crypto::Op::Decrypt); + return raw_read; +} + +template <bool Subsection, typename BlockType, typename BucketType> +std::pair<size_t, size_t> BKTR::SearchBucketEntry(u64 offset, BlockType block, + BucketType buckets) const { + if constexpr (Subsection) { + const auto last_bucket = buckets[block.number_buckets - 1]; + if (offset >= last_bucket.entries[last_bucket.number_entries].address_patch) + return {block.number_buckets - 1, last_bucket.number_entries}; + } else { + ASSERT_MSG(offset <= block.size, "Offset is out of bounds in BKTR relocation block."); + } + + size_t bucket_id = std::count_if(block.base_offsets.begin() + 1, + block.base_offsets.begin() + block.number_buckets, + [&offset](u64 base_offset) { return base_offset <= offset; }); + + const auto bucket = buckets[bucket_id]; + + if (bucket.number_entries == 1) + return {bucket_id, 0}; + + size_t low = 0; + size_t mid = 0; + size_t high = bucket.number_entries - 1; + while (low <= high) { + mid = (low + high) / 2; + if (bucket.entries[mid].address_patch > offset) { + high = mid - 1; + } else { + if (mid == bucket.number_entries - 1 || + bucket.entries[mid + 1].address_patch > offset) { + return {bucket_id, mid}; + } + + low = mid + 1; + } + } + + UNREACHABLE_MSG("Offset could not be found in BKTR block."); +} + +RelocationEntry BKTR::GetRelocationEntry(u64 offset) const { + const auto res = SearchBucketEntry<false>(offset, relocation, relocation_buckets); + return relocation_buckets[res.first].entries[res.second]; +} + +RelocationEntry BKTR::GetNextRelocationEntry(u64 offset) const { + const auto res = SearchBucketEntry<false>(offset, relocation, relocation_buckets); + const auto bucket = relocation_buckets[res.first]; + if (res.second + 1 < bucket.entries.size()) + return bucket.entries[res.second + 1]; + return relocation_buckets[res.first + 1].entries[0]; +} + +SubsectionEntry BKTR::GetSubsectionEntry(u64 offset) const { + const auto res = SearchBucketEntry<true>(offset, subsection, subsection_buckets); + return subsection_buckets[res.first].entries[res.second]; +} + +SubsectionEntry BKTR::GetNextSubsectionEntry(u64 offset) const { + const auto res = SearchBucketEntry<true>(offset, subsection, subsection_buckets); + const auto bucket = subsection_buckets[res.first]; + if (res.second + 1 < bucket.entries.size()) + return bucket.entries[res.second + 1]; + return subsection_buckets[res.first + 1].entries[0]; +} + +std::string BKTR::GetName() const { + return base_romfs->GetName(); +} + +size_t BKTR::GetSize() const { + return relocation.size; +} + +bool BKTR::Resize(size_t new_size) { + return false; +} + +std::shared_ptr<VfsDirectory> BKTR::GetContainingDirectory() const { + return base_romfs->GetContainingDirectory(); +} + +bool BKTR::IsWritable() const { + return false; +} + +bool BKTR::IsReadable() const { + return true; +} + +size_t BKTR::Write(const u8* data, size_t length, size_t offset) { + return 0; +} + +bool BKTR::Rename(std::string_view name) { + return base_romfs->Rename(name); +} + +} // namespace FileSys diff --git a/src/core/file_sys/nca_patch.h b/src/core/file_sys/nca_patch.h new file mode 100644 index 000000000..0d9ad95f5 --- /dev/null +++ b/src/core/file_sys/nca_patch.h @@ -0,0 +1,147 @@ +// Copyright 2018 yuzu emulator team +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <array> +#include <vector> +#include <common/common_funcs.h> +#include "core/crypto/key_manager.h" +#include "core/file_sys/romfs.h" + +namespace FileSys { + +#pragma pack(push, 1) +struct RelocationEntry { + u64_le address_patch; + u64_le address_source; + u32 from_patch; +}; +#pragma pack(pop) +static_assert(sizeof(RelocationEntry) == 0x14, "RelocationEntry has incorrect size."); + +struct RelocationBucketRaw { + INSERT_PADDING_BYTES(4); + u32_le number_entries; + u64_le end_offset; + std::array<RelocationEntry, 0x332> relocation_entries; + INSERT_PADDING_BYTES(8); +}; +static_assert(sizeof(RelocationBucketRaw) == 0x4000, "RelocationBucketRaw has incorrect size."); + +// Vector version of RelocationBucketRaw +struct RelocationBucket { + u32 number_entries; + u64 end_offset; + std::vector<RelocationEntry> entries; +}; + +struct RelocationBlock { + INSERT_PADDING_BYTES(4); + u32_le number_buckets; + u64_le size; + std::array<u64, 0x7FE> base_offsets; +}; +static_assert(sizeof(RelocationBlock) == 0x4000, "RelocationBlock has incorrect size."); + +struct SubsectionEntry { + u64_le address_patch; + INSERT_PADDING_BYTES(0x4); + u32_le ctr; +}; +static_assert(sizeof(SubsectionEntry) == 0x10, "SubsectionEntry has incorrect size."); + +struct SubsectionBucketRaw { + INSERT_PADDING_BYTES(4); + u32_le number_entries; + u64_le end_offset; + std::array<SubsectionEntry, 0x3FF> subsection_entries; +}; +static_assert(sizeof(SubsectionBucketRaw) == 0x4000, "SubsectionBucketRaw has incorrect size."); + +// Vector version of SubsectionBucketRaw +struct SubsectionBucket { + u32 number_entries; + u64 end_offset; + std::vector<SubsectionEntry> entries; +}; + +struct SubsectionBlock { + INSERT_PADDING_BYTES(4); + u32_le number_buckets; + u64_le size; + std::array<u64, 0x7FE> base_offsets; +}; +static_assert(sizeof(SubsectionBlock) == 0x4000, "SubsectionBlock has incorrect size."); + +inline RelocationBucket ConvertRelocationBucketRaw(RelocationBucketRaw raw) { + return {raw.number_entries, + raw.end_offset, + {raw.relocation_entries.begin(), raw.relocation_entries.begin() + raw.number_entries}}; +} + +inline SubsectionBucket ConvertSubsectionBucketRaw(SubsectionBucketRaw raw) { + return {raw.number_entries, + raw.end_offset, + {raw.subsection_entries.begin(), raw.subsection_entries.begin() + raw.number_entries}}; +} + +class BKTR : public VfsFile { +public: + BKTR(VirtualFile base_romfs, VirtualFile bktr_romfs, RelocationBlock relocation, + std::vector<RelocationBucket> relocation_buckets, SubsectionBlock subsection, + std::vector<SubsectionBucket> subsection_buckets, bool is_encrypted, + Core::Crypto::Key128 key, u64 base_offset, u64 ivfc_offset, std::array<u8, 8> section_ctr); + ~BKTR() override; + + size_t Read(u8* data, size_t length, size_t offset) const override; + + std::string GetName() const override; + + size_t GetSize() const override; + + bool Resize(size_t new_size) override; + + std::shared_ptr<VfsDirectory> GetContainingDirectory() const override; + + bool IsWritable() const override; + + bool IsReadable() const override; + + size_t Write(const u8* data, size_t length, size_t offset) override; + + bool Rename(std::string_view name) override; + +private: + template <bool Subsection, typename BlockType, typename BucketType> + std::pair<size_t, size_t> SearchBucketEntry(u64 offset, BlockType block, + BucketType buckets) const; + + RelocationEntry GetRelocationEntry(u64 offset) const; + RelocationEntry GetNextRelocationEntry(u64 offset) const; + + SubsectionEntry GetSubsectionEntry(u64 offset) const; + SubsectionEntry GetNextSubsectionEntry(u64 offset) const; + + RelocationBlock relocation; + std::vector<RelocationBucket> relocation_buckets; + SubsectionBlock subsection; + std::vector<SubsectionBucket> subsection_buckets; + + // Should be the raw base romfs, decrypted. + VirtualFile base_romfs; + // Should be the raw BKTR romfs, (located at media_offset with size media_size). + VirtualFile bktr_romfs; + + bool encrypted; + Core::Crypto::Key128 key; + + // Base offset into NCA, used for IV calculation. + u64 base_offset; + // Distance between IVFC start and RomFS start, used for base reads + u64 ivfc_offset; + std::array<u8, 8> section_ctr; +}; + +} // namespace FileSys diff --git a/src/core/file_sys/patch_manager.cpp b/src/core/file_sys/patch_manager.cpp new file mode 100644 index 000000000..40675de35 --- /dev/null +++ b/src/core/file_sys/patch_manager.cpp @@ -0,0 +1,153 @@ +// Copyright 2018 yuzu emulator team +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "core/file_sys/content_archive.h" +#include "core/file_sys/control_metadata.h" +#include "core/file_sys/patch_manager.h" +#include "core/file_sys/registered_cache.h" +#include "core/file_sys/romfs.h" +#include "core/hle/service/filesystem/filesystem.h" +#include "core/loader/loader.h" + +namespace FileSys { + +constexpr u64 SINGLE_BYTE_MODULUS = 0x100; + +std::string FormatTitleVersion(u32 version, TitleVersionFormat format) { + std::array<u8, sizeof(u32)> bytes{}; + bytes[0] = version % SINGLE_BYTE_MODULUS; + for (size_t i = 1; i < bytes.size(); ++i) { + version /= SINGLE_BYTE_MODULUS; + bytes[i] = version % SINGLE_BYTE_MODULUS; + } + + if (format == TitleVersionFormat::FourElements) + return fmt::format("v{}.{}.{}.{}", bytes[3], bytes[2], bytes[1], bytes[0]); + return fmt::format("v{}.{}.{}", bytes[3], bytes[2], bytes[1]); +} + +constexpr std::array<const char*, 1> PATCH_TYPE_NAMES{ + "Update", +}; + +std::string FormatPatchTypeName(PatchType type) { + return PATCH_TYPE_NAMES.at(static_cast<size_t>(type)); +} + +PatchManager::PatchManager(u64 title_id) : title_id(title_id) {} + +VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const { + LOG_INFO(Loader, "Patching ExeFS for title_id={:016X}", title_id); + + if (exefs == nullptr) + return exefs; + + const auto installed = Service::FileSystem::GetUnionContents(); + + // Game Updates + const auto update_tid = GetUpdateTitleID(title_id); + const auto update = installed->GetEntry(update_tid, ContentRecordType::Program); + if (update != nullptr) { + if (update->GetStatus() == Loader::ResultStatus::ErrorMissingBKTRBaseRomFS && + update->GetExeFS() != nullptr) { + LOG_INFO(Loader, " ExeFS: Update ({}) applied successfully", + FormatTitleVersion(installed->GetEntryVersion(update_tid).get_value_or(0))); + exefs = update->GetExeFS(); + } + } + + return exefs; +} + +VirtualFile PatchManager::PatchRomFS(VirtualFile romfs, u64 ivfc_offset, + ContentRecordType type) const { + LOG_INFO(Loader, "Patching RomFS for title_id={:016X}, type={:02X}", title_id, + static_cast<u8>(type)); + + if (romfs == nullptr) + return romfs; + + const auto installed = Service::FileSystem::GetUnionContents(); + + // Game Updates + const auto update_tid = GetUpdateTitleID(title_id); + const auto update = installed->GetEntryRaw(update_tid, type); + if (update != nullptr) { + const auto new_nca = std::make_shared<NCA>(update, romfs, ivfc_offset); + if (new_nca->GetStatus() == Loader::ResultStatus::Success && + new_nca->GetRomFS() != nullptr) { + LOG_INFO(Loader, " RomFS: Update ({}) applied successfully", + FormatTitleVersion(installed->GetEntryVersion(update_tid).get_value_or(0))); + romfs = new_nca->GetRomFS(); + } + } + + return romfs; +} + +std::map<PatchType, std::string> PatchManager::GetPatchVersionNames() const { + std::map<PatchType, std::string> out; + const auto installed = Service::FileSystem::GetUnionContents(); + + const auto update_tid = GetUpdateTitleID(title_id); + PatchManager update{update_tid}; + auto [nacp, discard_icon_file] = update.GetControlMetadata(); + + if (nacp != nullptr) { + out[PatchType::Update] = nacp->GetVersionString(); + } else { + if (installed->HasEntry(update_tid, ContentRecordType::Program)) { + const auto meta_ver = installed->GetEntryVersion(update_tid); + if (meta_ver == boost::none || meta_ver.get() == 0) { + out[PatchType::Update] = ""; + } else { + out[PatchType::Update] = + FormatTitleVersion(meta_ver.get(), TitleVersionFormat::ThreeElements); + } + } + } + + return out; +} + +std::pair<std::shared_ptr<NACP>, VirtualFile> PatchManager::GetControlMetadata() const { + const auto& installed{Service::FileSystem::GetUnionContents()}; + + const auto base_control_nca = installed->GetEntry(title_id, ContentRecordType::Control); + if (base_control_nca == nullptr) + return {}; + + return ParseControlNCA(base_control_nca); +} + +std::pair<std::shared_ptr<NACP>, VirtualFile> PatchManager::ParseControlNCA( + const std::shared_ptr<NCA>& nca) const { + const auto base_romfs = nca->GetRomFS(); + if (base_romfs == nullptr) + return {}; + + const auto romfs = PatchRomFS(base_romfs, nca->GetBaseIVFCOffset(), ContentRecordType::Control); + if (romfs == nullptr) + return {}; + + const auto extracted = ExtractRomFS(romfs); + if (extracted == nullptr) + return {}; + + auto nacp_file = extracted->GetFile("control.nacp"); + if (nacp_file == nullptr) + nacp_file = extracted->GetFile("Control.nacp"); + + const auto nacp = nacp_file == nullptr ? nullptr : std::make_shared<NACP>(nacp_file); + + VirtualFile icon_file; + for (const auto& language : FileSys::LANGUAGE_NAMES) { + icon_file = extracted->GetFile("icon_" + std::string(language) + ".dat"); + if (icon_file != nullptr) + break; + } + + return {nacp, icon_file}; +} +} // namespace FileSys diff --git a/src/core/file_sys/patch_manager.h b/src/core/file_sys/patch_manager.h new file mode 100644 index 000000000..28c7ae136 --- /dev/null +++ b/src/core/file_sys/patch_manager.h @@ -0,0 +1,62 @@ +// Copyright 2018 yuzu emulator team +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <map> +#include <string> +#include "common/common_types.h" +#include "core/file_sys/nca_metadata.h" +#include "core/file_sys/vfs.h" + +namespace FileSys { + +class NCA; +class NACP; + +enum class TitleVersionFormat : u8 { + ThreeElements, ///< vX.Y.Z + FourElements, ///< vX.Y.Z.W +}; + +std::string FormatTitleVersion(u32 version, + TitleVersionFormat format = TitleVersionFormat::ThreeElements); + +enum class PatchType { + Update, +}; + +std::string FormatPatchTypeName(PatchType type); + +// A centralized class to manage patches to games. +class PatchManager { +public: + explicit PatchManager(u64 title_id); + + // Currently tracked ExeFS patches: + // - Game Updates + VirtualDir PatchExeFS(VirtualDir exefs) const; + + // Currently tracked RomFS patches: + // - Game Updates + VirtualFile PatchRomFS(VirtualFile base, u64 ivfc_offset, + ContentRecordType type = ContentRecordType::Program) const; + + // Returns a vector of pairs between patch names and patch versions. + // i.e. Update v80 will return {Update, 80} + std::map<PatchType, std::string> GetPatchVersionNames() const; + + // Given title_id of the program, attempts to get the control data of the update and parse it, + // falling back to the base control data. + std::pair<std::shared_ptr<NACP>, VirtualFile> GetControlMetadata() const; + + // Version of GetControlMetadata that takes an arbitrary NCA + std::pair<std::shared_ptr<NACP>, VirtualFile> ParseControlNCA( + const std::shared_ptr<NCA>& nca) const; + +private: + u64 title_id; +}; + +} // namespace FileSys diff --git a/src/core/file_sys/registered_cache.cpp b/src/core/file_sys/registered_cache.cpp index cf6f77401..7361a67be 100644 --- a/src/core/file_sys/registered_cache.cpp +++ b/src/core/file_sys/registered_cache.cpp @@ -280,6 +280,18 @@ VirtualFile RegisteredCache::GetEntryUnparsed(RegisteredCacheEntry entry) const return GetEntryUnparsed(entry.title_id, entry.type); } +boost::optional<u32> RegisteredCache::GetEntryVersion(u64 title_id) const { + const auto meta_iter = meta.find(title_id); + if (meta_iter != meta.end()) + return meta_iter->second.GetTitleVersion(); + + const auto yuzu_meta_iter = yuzu_meta.find(title_id); + if (yuzu_meta_iter != yuzu_meta.end()) + return yuzu_meta_iter->second.GetTitleVersion(); + + return boost::none; +} + VirtualFile RegisteredCache::GetEntryRaw(u64 title_id, ContentRecordType type) const { const auto id = GetNcaIDFromMetadata(title_id, type); if (id == boost::none) @@ -498,4 +510,107 @@ bool RegisteredCache::RawInstallYuzuMeta(const CNMT& cnmt) { kv.second.GetTitleID() == cnmt.GetTitleID(); }) != yuzu_meta.end(); } + +RegisteredCacheUnion::RegisteredCacheUnion(std::vector<std::shared_ptr<RegisteredCache>> caches) + : caches(std::move(caches)) {} + +void RegisteredCacheUnion::Refresh() { + for (const auto& c : caches) + c->Refresh(); +} + +bool RegisteredCacheUnion::HasEntry(u64 title_id, ContentRecordType type) const { + return std::any_of(caches.begin(), caches.end(), [title_id, type](const auto& cache) { + return cache->HasEntry(title_id, type); + }); +} + +bool RegisteredCacheUnion::HasEntry(RegisteredCacheEntry entry) const { + return HasEntry(entry.title_id, entry.type); +} + +boost::optional<u32> RegisteredCacheUnion::GetEntryVersion(u64 title_id) const { + for (const auto& c : caches) { + const auto res = c->GetEntryVersion(title_id); + if (res != boost::none) + return res; + } + + return boost::none; +} + +VirtualFile RegisteredCacheUnion::GetEntryUnparsed(u64 title_id, ContentRecordType type) const { + for (const auto& c : caches) { + const auto res = c->GetEntryUnparsed(title_id, type); + if (res != nullptr) + return res; + } + + return nullptr; +} + +VirtualFile RegisteredCacheUnion::GetEntryUnparsed(RegisteredCacheEntry entry) const { + return GetEntryUnparsed(entry.title_id, entry.type); +} + +VirtualFile RegisteredCacheUnion::GetEntryRaw(u64 title_id, ContentRecordType type) const { + for (const auto& c : caches) { + const auto res = c->GetEntryRaw(title_id, type); + if (res != nullptr) + return res; + } + + return nullptr; +} + +VirtualFile RegisteredCacheUnion::GetEntryRaw(RegisteredCacheEntry entry) const { + return GetEntryRaw(entry.title_id, entry.type); +} + +std::shared_ptr<NCA> RegisteredCacheUnion::GetEntry(u64 title_id, ContentRecordType type) const { + const auto raw = GetEntryRaw(title_id, type); + if (raw == nullptr) + return nullptr; + return std::make_shared<NCA>(raw); +} + +std::shared_ptr<NCA> RegisteredCacheUnion::GetEntry(RegisteredCacheEntry entry) const { + return GetEntry(entry.title_id, entry.type); +} + +std::vector<RegisteredCacheEntry> RegisteredCacheUnion::ListEntries() const { + std::vector<RegisteredCacheEntry> out; + for (const auto& c : caches) { + c->IterateAllMetadata<RegisteredCacheEntry>( + out, + [](const CNMT& c, const ContentRecord& r) { + return RegisteredCacheEntry{c.GetTitleID(), r.type}; + }, + [](const CNMT& c, const ContentRecord& r) { return true; }); + } + return out; +} + +std::vector<RegisteredCacheEntry> RegisteredCacheUnion::ListEntriesFilter( + boost::optional<TitleType> title_type, boost::optional<ContentRecordType> record_type, + boost::optional<u64> title_id) const { + std::vector<RegisteredCacheEntry> out; + for (const auto& c : caches) { + c->IterateAllMetadata<RegisteredCacheEntry>( + out, + [](const CNMT& c, const ContentRecord& r) { + return RegisteredCacheEntry{c.GetTitleID(), r.type}; + }, + [&title_type, &record_type, &title_id](const CNMT& c, const ContentRecord& r) { + if (title_type != boost::none && title_type.get() != c.GetType()) + return false; + if (record_type != boost::none && record_type.get() != r.type) + return false; + if (title_id != boost::none && title_id.get() != c.GetTitleID()) + return false; + return true; + }); + } + return out; +} } // namespace FileSys diff --git a/src/core/file_sys/registered_cache.h b/src/core/file_sys/registered_cache.h index 467ceeef1..f487b0cf0 100644 --- a/src/core/file_sys/registered_cache.h +++ b/src/core/file_sys/registered_cache.h @@ -43,6 +43,10 @@ struct RegisteredCacheEntry { std::string DebugInfo() const; }; +constexpr u64 GetUpdateTitleID(u64 base_title_id) { + return base_title_id | 0x800; +} + // boost flat_map requires operator< for O(log(n)) lookups. bool operator<(const RegisteredCacheEntry& lhs, const RegisteredCacheEntry& rhs); @@ -60,6 +64,8 @@ bool operator<(const RegisteredCacheEntry& lhs, const RegisteredCacheEntry& rhs) * 4GB splitting can be ignored.) */ class RegisteredCache { + friend class RegisteredCacheUnion; + public: // Parsing function defines the conversion from raw file to NCA. If there are other steps // besides creating the NCA from the file (e.g. NAX0 on SD Card), that should go in a custom @@ -74,6 +80,8 @@ public: bool HasEntry(u64 title_id, ContentRecordType type) const; bool HasEntry(RegisteredCacheEntry entry) const; + boost::optional<u32> GetEntryVersion(u64 title_id) const; + VirtualFile GetEntryUnparsed(u64 title_id, ContentRecordType type) const; VirtualFile GetEntryUnparsed(RegisteredCacheEntry entry) const; @@ -131,4 +139,36 @@ private: boost::container::flat_map<u64, CNMT> yuzu_meta; }; +// Combines multiple RegisteredCaches (i.e. SysNAND, UserNAND, SDMC) into one interface. +class RegisteredCacheUnion { +public: + explicit RegisteredCacheUnion(std::vector<std::shared_ptr<RegisteredCache>> caches); + + void Refresh(); + + bool HasEntry(u64 title_id, ContentRecordType type) const; + bool HasEntry(RegisteredCacheEntry entry) const; + + boost::optional<u32> GetEntryVersion(u64 title_id) const; + + VirtualFile GetEntryUnparsed(u64 title_id, ContentRecordType type) const; + VirtualFile GetEntryUnparsed(RegisteredCacheEntry entry) const; + + VirtualFile GetEntryRaw(u64 title_id, ContentRecordType type) const; + VirtualFile GetEntryRaw(RegisteredCacheEntry entry) const; + + std::shared_ptr<NCA> GetEntry(u64 title_id, ContentRecordType type) const; + std::shared_ptr<NCA> GetEntry(RegisteredCacheEntry entry) const; + + std::vector<RegisteredCacheEntry> ListEntries() const; + // If a parameter is not boost::none, it will be filtered for from all entries. + std::vector<RegisteredCacheEntry> ListEntriesFilter( + boost::optional<TitleType> title_type = boost::none, + boost::optional<ContentRecordType> record_type = boost::none, + boost::optional<u64> title_id = boost::none) const; + +private: + std::vector<std::shared_ptr<RegisteredCache>> caches; +}; + } // namespace FileSys diff --git a/src/core/file_sys/romfs_factory.cpp b/src/core/file_sys/romfs_factory.cpp index 66f9786e0..d9d90939e 100644 --- a/src/core/file_sys/romfs_factory.cpp +++ b/src/core/file_sys/romfs_factory.cpp @@ -6,9 +6,13 @@ #include "common/assert.h" #include "common/common_types.h" #include "common/logging/log.h" +#include "core/core.h" #include "core/file_sys/content_archive.h" +#include "core/file_sys/nca_metadata.h" +#include "core/file_sys/patch_manager.h" #include "core/file_sys/registered_cache.h" #include "core/file_sys/romfs_factory.h" +#include "core/hle/kernel/process.h" #include "core/hle/service/filesystem/filesystem.h" #include "core/loader/loader.h" @@ -19,10 +23,17 @@ RomFSFactory::RomFSFactory(Loader::AppLoader& app_loader) { if (app_loader.ReadRomFS(file) != Loader::ResultStatus::Success) { LOG_ERROR(Service_FS, "Unable to read RomFS!"); } + + updatable = app_loader.IsRomFSUpdatable(); + ivfc_offset = app_loader.ReadRomFSIVFCOffset(); } ResultVal<VirtualFile> RomFSFactory::OpenCurrentProcess() { - return MakeResult<VirtualFile>(file); + if (!updatable) + return MakeResult<VirtualFile>(file); + + const PatchManager patch_manager(Core::CurrentProcess()->program_id); + return MakeResult<VirtualFile>(patch_manager.PatchRomFS(file, ivfc_offset)); } ResultVal<VirtualFile> RomFSFactory::Open(u64 title_id, StorageId storage, ContentRecordType type) { diff --git a/src/core/file_sys/romfs_factory.h b/src/core/file_sys/romfs_factory.h index f38ddc4f7..26b8f46cc 100644 --- a/src/core/file_sys/romfs_factory.h +++ b/src/core/file_sys/romfs_factory.h @@ -36,6 +36,8 @@ public: private: VirtualFile file; + bool updatable; + u64 ivfc_offset; }; } // namespace FileSys diff --git a/src/core/file_sys/submission_package.cpp b/src/core/file_sys/submission_package.cpp index bde879861..11264878d 100644 --- a/src/core/file_sys/submission_package.cpp +++ b/src/core/file_sys/submission_package.cpp @@ -2,9 +2,15 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include <algorithm> +#include <cstring> +#include <string_view> + #include <fmt/ostream.h> -#include "common/assert.h" + #include "common/hex_util.h" +#include "common/logging/log.h" +#include "core/crypto/key_manager.h" #include "core/file_sys/content_archive.h" #include "core/file_sys/nca_metadata.h" #include "core/file_sys/partition_filesystem.h" @@ -13,8 +19,8 @@ namespace FileSys { NSP::NSP(VirtualFile file_) - : file(std::move(file_)), - pfs(std::make_shared<PartitionFilesystem>(file)), status{Loader::ResultStatus::Success} { + : file(std::move(file_)), status{Loader::ResultStatus::Success}, + pfs(std::make_shared<PartitionFilesystem>(file)) { if (pfs->GetStatus() != Loader::ResultStatus::Success) { status = pfs->GetStatus(); return; @@ -60,8 +66,11 @@ NSP::NSP(VirtualFile file_) for (const auto& outer_file : files) { if (outer_file->GetName().substr(outer_file->GetName().size() - 9) == ".cnmt.nca") { const auto nca = std::make_shared<NCA>(outer_file); - if (nca->GetStatus() != Loader::ResultStatus::Success) + if (nca->GetStatus() != Loader::ResultStatus::Success) { + program_status[nca->GetTitleId()] = nca->GetStatus(); continue; + } + const auto section0 = nca->GetSubdirectories()[0]; for (const auto& inner_file : section0->GetFiles()) { diff --git a/src/core/file_sys/submission_package.h b/src/core/file_sys/submission_package.h index 0292164f9..1120a4920 100644 --- a/src/core/file_sys/submission_package.h +++ b/src/core/file_sys/submission_package.h @@ -4,20 +4,23 @@ #pragma once -#include <array> #include <map> +#include <memory> #include <vector> #include "common/common_types.h" -#include "common/swap.h" -#include "core/file_sys/content_archive.h" -#include "core/file_sys/romfs_factory.h" #include "core/file_sys/vfs.h" -#include "core/loader/loader.h" + +namespace Loader { +enum class ResultStatus : u16; +} namespace FileSys { +class NCA; class PartitionFilesystem; +enum class ContentRecordType : u8; + class NSP : public ReadOnlyVfsDirectory { public: explicit NSP(VirtualFile file); |