Merge pull request #1179 from DarkLordZach/bktr

file_sys: Add support for BKTR format (Game Updates)
merge-requests/60/head
bunnei 2018-09-05 18:06:11 +07:00 committed by GitHub
commit a6ae765410
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1132 additions and 101 deletions

@ -35,8 +35,12 @@ add_library(core STATIC
file_sys/mode.h file_sys/mode.h
file_sys/nca_metadata.cpp file_sys/nca_metadata.cpp
file_sys/nca_metadata.h file_sys/nca_metadata.h
file_sys/nca_patch.cpp
file_sys/nca_patch.h
file_sys/partition_filesystem.cpp file_sys/partition_filesystem.cpp
file_sys/partition_filesystem.h file_sys/partition_filesystem.h
file_sys/patch_manager.cpp
file_sys/patch_manager.h
file_sys/program_metadata.cpp file_sys/program_metadata.cpp
file_sys/program_metadata.h file_sys/program_metadata.h
file_sys/registered_cache.cpp file_sys/registered_cache.cpp

@ -82,11 +82,25 @@ void AESCipher<Key, KeySize>::Transcode(const u8* src, size_t size, u8* dest, Op
} }
} else { } else {
const auto block_size = mbedtls_cipher_get_block_size(context); const auto block_size = mbedtls_cipher_get_block_size(context);
if (size < block_size) {
std::vector<u8> block(block_size);
std::memcpy(block.data(), src, size);
Transcode(block.data(), block.size(), block.data(), op);
std::memcpy(dest, block.data(), size);
return;
}
for (size_t offset = 0; offset < size; offset += block_size) { for (size_t offset = 0; offset < size; offset += block_size) {
auto length = std::min<size_t>(block_size, size - offset); auto length = std::min<size_t>(block_size, size - offset);
mbedtls_cipher_update(context, src + offset, length, dest + offset, &written); mbedtls_cipher_update(context, src + offset, length, dest + offset, &written);
if (written != length) { if (written != length) {
if (length < block_size) {
std::vector<u8> block(block_size);
std::memcpy(block.data(), src + offset, length);
Transcode(block.data(), block.size(), block.data(), op);
std::memcpy(dest + offset, block.data(), length);
return;
}
LOG_WARNING(Crypto, "Not all data was decrypted requested={:016X}, actual={:016X}.", LOG_WARNING(Crypto, "Not all data was decrypted requested={:016X}, actual={:016X}.",
length, written); length, written);
} }

@ -21,7 +21,7 @@ size_t CTREncryptionLayer::Read(u8* data, size_t length, size_t offset) const {
UpdateIV(base_offset + offset); UpdateIV(base_offset + offset);
std::vector<u8> raw = base->ReadBytes(length, offset); std::vector<u8> raw = base->ReadBytes(length, offset);
cipher.Transcode(raw.data(), raw.size(), data, Op::Decrypt); cipher.Transcode(raw.data(), raw.size(), data, Op::Decrypt);
return raw.size(); return length;
} }
// offset does not fall on block boundary (0x10) // offset does not fall on block boundary (0x10)

@ -52,11 +52,11 @@ XCI::XCI(VirtualFile file_) : file(std::move(file_)), partitions(0x4) {
const auto secure_ncas = secure_partition->GetNCAsCollapsed(); const auto secure_ncas = secure_partition->GetNCAsCollapsed();
std::copy(secure_ncas.begin(), secure_ncas.end(), std::back_inserter(ncas)); std::copy(secure_ncas.begin(), secure_ncas.end(), std::back_inserter(ncas));
program_nca_status = Loader::ResultStatus::ErrorXCIMissingProgramNCA;
program = program =
secure_partition->GetNCA(secure_partition->GetProgramTitleID(), ContentRecordType::Program); secure_partition->GetNCA(secure_partition->GetProgramTitleID(), ContentRecordType::Program);
if (program != nullptr) program_nca_status = secure_partition->GetProgramStatus(secure_partition->GetProgramTitleID());
program_nca_status = program->GetStatus(); if (program_nca_status == Loader::ResultStatus::ErrorNSPMissingProgramNCA)
program_nca_status = Loader::ResultStatus::ErrorXCIMissingProgramNCA;
auto result = AddNCAFromPartition(XCIPartition::Update); auto result = AddNCAFromPartition(XCIPartition::Update);
if (result != Loader::ResultStatus::Success) { if (result != Loader::ResultStatus::Success) {

@ -12,6 +12,7 @@
#include "core/crypto/aes_util.h" #include "core/crypto/aes_util.h"
#include "core/crypto/ctr_encryption_layer.h" #include "core/crypto/ctr_encryption_layer.h"
#include "core/file_sys/content_archive.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/partition_filesystem.h"
#include "core/file_sys/romfs.h" #include "core/file_sys/romfs.h"
#include "core/file_sys/vfs_offset.h" #include "core/file_sys/vfs_offset.h"
@ -68,10 +69,31 @@ struct RomFSSuperblock {
}; };
static_assert(sizeof(RomFSSuperblock) == 0x200, "RomFSSuperblock has incorrect size."); 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 { union NCASectionHeader {
NCASectionRaw raw; NCASectionRaw raw;
PFS0Superblock pfs0; PFS0Superblock pfs0;
RomFSSuperblock romfs; RomFSSuperblock romfs;
BKTRSuperblock bktr;
}; };
static_assert(sizeof(NCASectionHeader) == 0x200, "NCASectionHeader has incorrect size."); 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; Core::Crypto::Key128 out;
if (type == NCASectionCryptoType::XTS) if (type == NCASectionCryptoType::XTS)
std::copy(key_area.begin(), key_area.begin() + 0x10, out.begin()); 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()); std::copy(key_area.begin() + 0x20, key_area.begin() + 0x30, out.begin());
else else
LOG_CRITICAL(Crypto, "Called GetKeyAreaKey on invalid NCASectionCryptoType type={:02X}", 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"); LOG_DEBUG(Crypto, "called with mode=NONE");
return in; return in;
case NCASectionCryptoType::CTR: 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); LOG_DEBUG(Crypto, "called with mode=CTR, starting_offset={:016X}", starting_offset);
{ {
boost::optional<Core::Crypto::Key128> key = boost::none; 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; status = Loader::ResultStatus::Success;
if (file == nullptr) { 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) { is_update = std::find_if(sections.begin(), sections.end(), [](const NCASectionHeader& header) {
return header.raw.header.crypto_type == NCASectionCryptoType::BKTR; return header.raw.header.crypto_type == NCASectionCryptoType::BKTR;
}) != sections.end(); }) != sections.end();
ivfc_offset = 0;
for (std::ptrdiff_t i = 0; i < number_sections; ++i) { for (std::ptrdiff_t i = 0; i < number_sections; ++i) {
auto section = sections[i]; auto section = sections[i];
if (section.raw.header.filesystem_type == NCASectionFilesystemType::ROMFS) { if (section.raw.header.filesystem_type == NCASectionFilesystemType::ROMFS) {
const size_t romfs_offset = const size_t base_offset =
header.section_tables[i].media_offset * MEDIA_OFFSET_MULTIPLIER + header.section_tables[i].media_offset * MEDIA_OFFSET_MULTIPLIER;
section.romfs.ivfc.levels[IVFC_MAX_LEVEL - 1].offset; 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; const size_t romfs_size = section.romfs.ivfc.levels[IVFC_MAX_LEVEL - 1].size;
auto dec = auto raw = std::make_shared<OffsetVfsFile>(file, romfs_size, romfs_offset);
Decrypt(section, std::make_shared<OffsetVfsFile>(file, romfs_size, romfs_offset), auto dec = Decrypt(section, raw, romfs_offset);
romfs_offset);
if (dec != nullptr) { if (dec == nullptr) {
files.push_back(std::move(dec));
romfs = files.back();
} else {
if (status != Loader::ResultStatus::Success) if (status != Loader::ResultStatus::Success)
return; return;
if (has_rights_id) if (has_rights_id)
@ -289,6 +315,117 @@ NCA::NCA(VirtualFile file_) : file(std::move(file_)) {
status = Loader::ResultStatus::ErrorIncorrectKeyAreaKey; status = Loader::ResultStatus::ErrorIncorrectKeyAreaKey;
return; 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) { } else if (section.raw.header.filesystem_type == NCASectionFilesystemType::PFS0) {
u64 offset = (static_cast<u64>(header.section_tables[i].media_offset) * u64 offset = (static_cast<u64>(header.section_tables[i].media_offset) *
MEDIA_OFFSET_MULTIPLIER) + MEDIA_OFFSET_MULTIPLIER) +
@ -304,6 +441,12 @@ NCA::NCA(VirtualFile file_) : file(std::move(file_)) {
dirs.push_back(std::move(npfs)); dirs.push_back(std::move(npfs));
if (IsDirectoryExeFS(dirs.back())) if (IsDirectoryExeFS(dirs.back()))
exefs = dirs.back(); exefs = dirs.back();
} else {
if (has_rights_id)
status = Loader::ResultStatus::ErrorIncorrectTitlekeyOrTitlekek;
else
status = Loader::ResultStatus::ErrorIncorrectKeyAreaKey;
return;
} }
} else { } else {
if (status != Loader::ResultStatus::Success) if (status != Loader::ResultStatus::Success)
@ -349,11 +492,15 @@ NCAContentType NCA::GetType() const {
} }
u64 NCA::GetTitleId() const { u64 NCA::GetTitleId() const {
if (status != Loader::ResultStatus::Success) if (is_update || status == Loader::ResultStatus::ErrorMissingBKTRBaseRomFS)
return {}; return header.title_id | 0x800;
return header.title_id; return header.title_id;
} }
bool NCA::IsUpdate() const {
return is_update;
}
VirtualFile NCA::GetRomFS() const { VirtualFile NCA::GetRomFS() const {
return romfs; return romfs;
} }
@ -366,8 +513,8 @@ VirtualFile NCA::GetBaseFile() const {
return file; return file;
} }
bool NCA::IsUpdate() const { u64 NCA::GetBaseIVFCOffset() const {
return is_update; return ivfc_offset;
} }
bool NCA::ReplaceFileWithSubdirectory(VirtualFile file, VirtualDir dir) { bool NCA::ReplaceFileWithSubdirectory(VirtualFile file, VirtualDir dir) {

@ -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. // After construction, use GetStatus to determine if the file is valid and ready to be used.
class NCA : public ReadOnlyVfsDirectory { class NCA : public ReadOnlyVfsDirectory {
public: public:
explicit NCA(VirtualFile file); explicit NCA(VirtualFile file, VirtualFile bktr_base_romfs = nullptr,
u64 bktr_base_ivfc_offset = 0);
Loader::ResultStatus GetStatus() const; Loader::ResultStatus GetStatus() const;
std::vector<std::shared_ptr<VfsFile>> GetFiles() const override; std::vector<std::shared_ptr<VfsFile>> GetFiles() const override;
@ -89,13 +90,15 @@ public:
NCAContentType GetType() const; NCAContentType GetType() const;
u64 GetTitleId() const; u64 GetTitleId() const;
bool IsUpdate() const;
VirtualFile GetRomFS() const; VirtualFile GetRomFS() const;
VirtualDir GetExeFS() const; VirtualDir GetExeFS() const;
VirtualFile GetBaseFile() const; VirtualFile GetBaseFile() const;
bool IsUpdate() const; // Returns the base ivfc offset used in BKTR patching.
u64 GetBaseIVFCOffset() const;
protected: protected:
bool ReplaceFileWithSubdirectory(VirtualFile file, VirtualDir dir) override; bool ReplaceFileWithSubdirectory(VirtualFile file, VirtualDir dir) override;
@ -112,14 +115,16 @@ private:
VirtualFile romfs = nullptr; VirtualFile romfs = nullptr;
VirtualDir exefs = nullptr; VirtualDir exefs = nullptr;
VirtualFile file; VirtualFile file;
VirtualFile bktr_base_romfs;
u64 ivfc_offset;
NCAHeader header{}; NCAHeader header{};
bool has_rights_id{}; bool has_rights_id{};
bool is_update{};
Loader::ResultStatus status{}; Loader::ResultStatus status{};
bool encrypted; bool encrypted;
bool is_update;
Core::Crypto::KeyManager keys; Core::Crypto::KeyManager keys;
}; };

@ -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

@ -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

@ -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

@ -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

@ -280,6 +280,18 @@ VirtualFile RegisteredCache::GetEntryUnparsed(RegisteredCacheEntry entry) const
return GetEntryUnparsed(entry.title_id, entry.type); 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 { VirtualFile RegisteredCache::GetEntryRaw(u64 title_id, ContentRecordType type) const {
const auto id = GetNcaIDFromMetadata(title_id, type); const auto id = GetNcaIDFromMetadata(title_id, type);
if (id == boost::none) if (id == boost::none)
@ -498,4 +510,107 @@ bool RegisteredCache::RawInstallYuzuMeta(const CNMT& cnmt) {
kv.second.GetTitleID() == cnmt.GetTitleID(); kv.second.GetTitleID() == cnmt.GetTitleID();
}) != yuzu_meta.end(); }) != 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 } // namespace FileSys

@ -43,6 +43,10 @@ struct RegisteredCacheEntry {
std::string DebugInfo() const; 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. // boost flat_map requires operator< for O(log(n)) lookups.
bool operator<(const RegisteredCacheEntry& lhs, const RegisteredCacheEntry& rhs); 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.) * 4GB splitting can be ignored.)
*/ */
class RegisteredCache { class RegisteredCache {
friend class RegisteredCacheUnion;
public: public:
// Parsing function defines the conversion from raw file to NCA. If there are other steps // 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 // 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(u64 title_id, ContentRecordType type) const;
bool HasEntry(RegisteredCacheEntry entry) const; bool HasEntry(RegisteredCacheEntry entry) const;
boost::optional<u32> GetEntryVersion(u64 title_id) const;
VirtualFile GetEntryUnparsed(u64 title_id, ContentRecordType type) const; VirtualFile GetEntryUnparsed(u64 title_id, ContentRecordType type) const;
VirtualFile GetEntryUnparsed(RegisteredCacheEntry entry) const; VirtualFile GetEntryUnparsed(RegisteredCacheEntry entry) const;
@ -131,4 +139,36 @@ private:
boost::container::flat_map<u64, CNMT> yuzu_meta; 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 } // namespace FileSys

@ -6,9 +6,13 @@
#include "common/assert.h" #include "common/assert.h"
#include "common/common_types.h" #include "common/common_types.h"
#include "common/logging/log.h" #include "common/logging/log.h"
#include "core/core.h"
#include "core/file_sys/content_archive.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/registered_cache.h"
#include "core/file_sys/romfs_factory.h" #include "core/file_sys/romfs_factory.h"
#include "core/hle/kernel/process.h"
#include "core/hle/service/filesystem/filesystem.h" #include "core/hle/service/filesystem/filesystem.h"
#include "core/loader/loader.h" #include "core/loader/loader.h"
@ -19,10 +23,17 @@ RomFSFactory::RomFSFactory(Loader::AppLoader& app_loader) {
if (app_loader.ReadRomFS(file) != Loader::ResultStatus::Success) { if (app_loader.ReadRomFS(file) != Loader::ResultStatus::Success) {
LOG_ERROR(Service_FS, "Unable to read RomFS!"); LOG_ERROR(Service_FS, "Unable to read RomFS!");
} }
updatable = app_loader.IsRomFSUpdatable();
ivfc_offset = app_loader.ReadRomFSIVFCOffset();
} }
ResultVal<VirtualFile> RomFSFactory::OpenCurrentProcess() { 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) { ResultVal<VirtualFile> RomFSFactory::Open(u64 title_id, StorageId storage, ContentRecordType type) {

@ -36,6 +36,8 @@ public:
private: private:
VirtualFile file; VirtualFile file;
bool updatable;
u64 ivfc_offset;
}; };
} // namespace FileSys } // namespace FileSys

@ -60,8 +60,11 @@ NSP::NSP(VirtualFile file_)
for (const auto& outer_file : files) { for (const auto& outer_file : files) {
if (outer_file->GetName().substr(outer_file->GetName().size() - 9) == ".cnmt.nca") { if (outer_file->GetName().substr(outer_file->GetName().size() - 9) == ".cnmt.nca") {
const auto nca = std::make_shared<NCA>(outer_file); 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; continue;
}
const auto section0 = nca->GetSubdirectories()[0]; const auto section0 = nca->GetSubdirectories()[0];
for (const auto& inner_file : section0->GetFiles()) { for (const auto& inner_file : section0->GetFiles()) {

@ -10,6 +10,7 @@
#include "core/file_sys/bis_factory.h" #include "core/file_sys/bis_factory.h"
#include "core/file_sys/errors.h" #include "core/file_sys/errors.h"
#include "core/file_sys/mode.h" #include "core/file_sys/mode.h"
#include "core/file_sys/registered_cache.h"
#include "core/file_sys/romfs_factory.h" #include "core/file_sys/romfs_factory.h"
#include "core/file_sys/savedata_factory.h" #include "core/file_sys/savedata_factory.h"
#include "core/file_sys/sdmc_factory.h" #include "core/file_sys/sdmc_factory.h"
@ -307,6 +308,12 @@ ResultVal<FileSys::VirtualDir> OpenSDMC() {
return sdmc_factory->Open(); return sdmc_factory->Open();
} }
std::shared_ptr<FileSys::RegisteredCacheUnion> GetUnionContents() {
return std::make_shared<FileSys::RegisteredCacheUnion>(
std::vector<std::shared_ptr<FileSys::RegisteredCache>>{
GetSystemNANDContents(), GetUserNANDContents(), GetSDMCContents()});
}
std::shared_ptr<FileSys::RegisteredCache> GetSystemNANDContents() { std::shared_ptr<FileSys::RegisteredCache> GetSystemNANDContents() {
LOG_TRACE(Service_FS, "Opening System NAND Contents"); LOG_TRACE(Service_FS, "Opening System NAND Contents");

@ -13,6 +13,7 @@
namespace FileSys { namespace FileSys {
class BISFactory; class BISFactory;
class RegisteredCache; class RegisteredCache;
class RegisteredCacheUnion;
class RomFSFactory; class RomFSFactory;
class SaveDataFactory; class SaveDataFactory;
class SDMCFactory; class SDMCFactory;
@ -45,6 +46,8 @@ ResultVal<FileSys::VirtualDir> OpenSaveData(FileSys::SaveDataSpaceId space,
FileSys::SaveDataDescriptor save_struct); FileSys::SaveDataDescriptor save_struct);
ResultVal<FileSys::VirtualDir> OpenSDMC(); ResultVal<FileSys::VirtualDir> OpenSDMC();
std::shared_ptr<FileSys::RegisteredCacheUnion> GetUnionContents();
std::shared_ptr<FileSys::RegisteredCache> GetSystemNANDContents(); std::shared_ptr<FileSys::RegisteredCache> GetSystemNANDContents();
std::shared_ptr<FileSys::RegisteredCache> GetUserNANDContents(); std::shared_ptr<FileSys::RegisteredCache> GetUserNANDContents();
std::shared_ptr<FileSys::RegisteredCache> GetSDMCContents(); std::shared_ptr<FileSys::RegisteredCache> GetSDMCContents();

@ -9,6 +9,7 @@
#include "core/core.h" #include "core/core.h"
#include "core/file_sys/content_archive.h" #include "core/file_sys/content_archive.h"
#include "core/file_sys/control_metadata.h" #include "core/file_sys/control_metadata.h"
#include "core/file_sys/patch_manager.h"
#include "core/file_sys/romfs_factory.h" #include "core/file_sys/romfs_factory.h"
#include "core/gdbstub/gdbstub.h" #include "core/gdbstub/gdbstub.h"
#include "core/hle/kernel/kernel.h" #include "core/hle/kernel/kernel.h"
@ -21,10 +22,19 @@
namespace Loader { namespace Loader {
AppLoader_DeconstructedRomDirectory::AppLoader_DeconstructedRomDirectory(FileSys::VirtualFile file_) AppLoader_DeconstructedRomDirectory::AppLoader_DeconstructedRomDirectory(FileSys::VirtualFile file_,
: AppLoader(std::move(file_)) { bool override_update)
: AppLoader(std::move(file_)), override_update(override_update) {
const auto dir = file->GetContainingDirectory(); const auto dir = file->GetContainingDirectory();
// Title ID
const auto npdm = dir->GetFile("main.npdm");
if (npdm != nullptr) {
const auto res = metadata.Load(npdm);
if (res == ResultStatus::Success)
title_id = metadata.GetTitleID();
}
// Icon // Icon
FileSys::VirtualFile icon_file = nullptr; FileSys::VirtualFile icon_file = nullptr;
for (const auto& language : FileSys::LANGUAGE_NAMES) { for (const auto& language : FileSys::LANGUAGE_NAMES) {
@ -66,8 +76,9 @@ AppLoader_DeconstructedRomDirectory::AppLoader_DeconstructedRomDirectory(FileSys
} }
AppLoader_DeconstructedRomDirectory::AppLoader_DeconstructedRomDirectory( AppLoader_DeconstructedRomDirectory::AppLoader_DeconstructedRomDirectory(
FileSys::VirtualDir directory) FileSys::VirtualDir directory, bool override_update)
: AppLoader(directory->GetFile("main")), dir(std::move(directory)) {} : AppLoader(directory->GetFile("main")), dir(std::move(directory)),
override_update(override_update) {}
FileType AppLoader_DeconstructedRomDirectory::IdentifyType(const FileSys::VirtualFile& file) { FileType AppLoader_DeconstructedRomDirectory::IdentifyType(const FileSys::VirtualFile& file) {
if (FileSys::IsDirectoryExeFS(file->GetContainingDirectory())) { if (FileSys::IsDirectoryExeFS(file->GetContainingDirectory())) {
@ -89,7 +100,8 @@ ResultStatus AppLoader_DeconstructedRomDirectory::Load(
dir = file->GetContainingDirectory(); dir = file->GetContainingDirectory();
} }
const FileSys::VirtualFile npdm = dir->GetFile("main.npdm"); // Read meta to determine title ID
FileSys::VirtualFile npdm = dir->GetFile("main.npdm");
if (npdm == nullptr) if (npdm == nullptr)
return ResultStatus::ErrorMissingNPDM; return ResultStatus::ErrorMissingNPDM;
@ -97,6 +109,21 @@ ResultStatus AppLoader_DeconstructedRomDirectory::Load(
if (result != ResultStatus::Success) { if (result != ResultStatus::Success) {
return result; return result;
} }
if (override_update) {
const FileSys::PatchManager patch_manager(metadata.GetTitleID());
dir = patch_manager.PatchExeFS(dir);
}
// Reread in case PatchExeFS affected the main.npdm
npdm = dir->GetFile("main.npdm");
if (npdm == nullptr)
return ResultStatus::ErrorMissingNPDM;
ResultStatus result2 = metadata.Load(npdm);
if (result2 != ResultStatus::Success) {
return result2;
}
metadata.Print(); metadata.Print();
const FileSys::ProgramAddressSpaceType arch_bits{metadata.GetAddressSpaceType()}; const FileSys::ProgramAddressSpaceType arch_bits{metadata.GetAddressSpaceType()};
@ -119,7 +146,6 @@ ResultStatus AppLoader_DeconstructedRomDirectory::Load(
} }
auto& kernel = Core::System::GetInstance().Kernel(); auto& kernel = Core::System::GetInstance().Kernel();
title_id = metadata.GetTitleID();
process->program_id = metadata.GetTitleID(); process->program_id = metadata.GetTitleID();
process->svc_access_mask.set(); process->svc_access_mask.set();
process->resource_limit = process->resource_limit =
@ -170,4 +196,8 @@ ResultStatus AppLoader_DeconstructedRomDirectory::ReadTitle(std::string& title)
return ResultStatus::Success; return ResultStatus::Success;
} }
bool AppLoader_DeconstructedRomDirectory::IsRomFSUpdatable() const {
return false;
}
} // namespace Loader } // namespace Loader

@ -20,10 +20,12 @@ namespace Loader {
*/ */
class AppLoader_DeconstructedRomDirectory final : public AppLoader { class AppLoader_DeconstructedRomDirectory final : public AppLoader {
public: public:
explicit AppLoader_DeconstructedRomDirectory(FileSys::VirtualFile main_file); explicit AppLoader_DeconstructedRomDirectory(FileSys::VirtualFile main_file,
bool override_update = false);
// Overload to accept exefs directory. Must contain 'main' and 'main.npdm' // Overload to accept exefs directory. Must contain 'main' and 'main.npdm'
explicit AppLoader_DeconstructedRomDirectory(FileSys::VirtualDir directory); explicit AppLoader_DeconstructedRomDirectory(FileSys::VirtualDir directory,
bool override_update = false);
/** /**
* Returns the type of the file * Returns the type of the file
@ -42,6 +44,7 @@ public:
ResultStatus ReadIcon(std::vector<u8>& buffer) override; ResultStatus ReadIcon(std::vector<u8>& buffer) override;
ResultStatus ReadProgramId(u64& out_program_id) override; ResultStatus ReadProgramId(u64& out_program_id) override;
ResultStatus ReadTitle(std::string& title) override; ResultStatus ReadTitle(std::string& title) override;
bool IsRomFSUpdatable() const override;
private: private:
FileSys::ProgramMetadata metadata; FileSys::ProgramMetadata metadata;
@ -51,6 +54,7 @@ private:
std::vector<u8> icon_data; std::vector<u8> icon_data;
std::string name; std::string name;
u64 title_id{}; u64 title_id{};
bool override_update;
}; };
} // namespace Loader } // namespace Loader

@ -93,7 +93,7 @@ std::string GetFileTypeString(FileType type) {
return "unknown"; return "unknown";
} }
constexpr std::array<const char*, 50> RESULT_MESSAGES{ constexpr std::array<const char*, 58> RESULT_MESSAGES{
"The operation completed successfully.", "The operation completed successfully.",
"The loader requested to load is already loaded.", "The loader requested to load is already loaded.",
"The operation is not implemented.", "The operation is not implemented.",
@ -143,7 +143,16 @@ constexpr std::array<const char*, 50> RESULT_MESSAGES{
"The AES Key Generation Source could not be found.", "The AES Key Generation Source could not be found.",
"The SD Save Key Source could not be found.", "The SD Save Key Source could not be found.",
"The SD NCA Key Source could not be found.", "The SD NCA Key Source could not be found.",
"The NSP file is missing a Program-type NCA."}; "The NSP file is missing a Program-type NCA.",
"The BKTR-type NCA has a bad BKTR header.",
"The BKTR Subsection entry is not located immediately after the Relocation entry.",
"The BKTR Subsection entry is not at the end of the media block.",
"The BKTR-type NCA has a bad Relocation block.",
"The BKTR-type NCA has a bad Subsection block.",
"The BKTR-type NCA has a bad Relocation bucket.",
"The BKTR-type NCA has a bad Subsection bucket.",
"The BKTR-type NCA is missing the base RomFS.",
};
std::ostream& operator<<(std::ostream& os, ResultStatus status) { std::ostream& operator<<(std::ostream& os, ResultStatus status) {
os << RESULT_MESSAGES.at(static_cast<size_t>(status)); os << RESULT_MESSAGES.at(static_cast<size_t>(status));

@ -107,6 +107,14 @@ enum class ResultStatus : u16 {
ErrorMissingSDSaveKeySource, ErrorMissingSDSaveKeySource,
ErrorMissingSDNCAKeySource, ErrorMissingSDNCAKeySource,
ErrorNSPMissingProgramNCA, ErrorNSPMissingProgramNCA,
ErrorBadBKTRHeader,
ErrorBKTRSubsectionNotAfterRelocation,
ErrorBKTRSubsectionNotAtEnd,
ErrorBadRelocationBlock,
ErrorBadSubsectionBlock,
ErrorBadRelocationBuckets,
ErrorBadSubsectionBuckets,
ErrorMissingBKTRBaseRomFS,
}; };
std::ostream& operator<<(std::ostream& os, ResultStatus status); std::ostream& operator<<(std::ostream& os, ResultStatus status);
@ -197,13 +205,22 @@ public:
} }
/** /**
* Get the update RomFS of the application * Get whether or not updates can be applied to the RomFS.
* Since the RomFS can be huge, we return a file reference instead of copying to a buffer * By default, this is true, however for formats where it cannot be guaranteed that the RomFS is
* @param file The file containing the RomFS * the base game it should be set to false.
* @return ResultStatus result of function * @return bool whether or not updatable.
*/ */
virtual ResultStatus ReadUpdateRomFS(FileSys::VirtualFile& file) { virtual bool IsRomFSUpdatable() const {
return ResultStatus::ErrorNotImplemented; return true;
}
/**
* Gets the difference between the start of the IVFC header and the start of level 6 (RomFS)
* data. Needed for bktr patching.
* @return IVFC offset for romfs.
*/
virtual u64 ReadRomFSIVFCOffset() const {
return 0;
} }
/** /**

@ -48,7 +48,7 @@ ResultStatus AppLoader_NCA::Load(Kernel::SharedPtr<Kernel::Process>& process) {
if (exefs == nullptr) if (exefs == nullptr)
return ResultStatus::ErrorNoExeFS; return ResultStatus::ErrorNoExeFS;
directory_loader = std::make_unique<AppLoader_DeconstructedRomDirectory>(exefs); directory_loader = std::make_unique<AppLoader_DeconstructedRomDirectory>(exefs, true);
const auto load_result = directory_loader->Load(process); const auto load_result = directory_loader->Load(process);
if (load_result != ResultStatus::Success) if (load_result != ResultStatus::Success)
@ -71,6 +71,12 @@ ResultStatus AppLoader_NCA::ReadRomFS(FileSys::VirtualFile& dir) {
return ResultStatus::Success; return ResultStatus::Success;
} }
u64 AppLoader_NCA::ReadRomFSIVFCOffset() const {
if (nca == nullptr)
return 0;
return nca->GetBaseIVFCOffset();
}
ResultStatus AppLoader_NCA::ReadProgramId(u64& out_program_id) { ResultStatus AppLoader_NCA::ReadProgramId(u64& out_program_id) {
if (nca == nullptr || nca->GetStatus() != ResultStatus::Success) if (nca == nullptr || nca->GetStatus() != ResultStatus::Success)
return ResultStatus::ErrorNotInitialized; return ResultStatus::ErrorNotInitialized;

@ -37,6 +37,7 @@ public:
ResultStatus Load(Kernel::SharedPtr<Kernel::Process>& process) override; ResultStatus Load(Kernel::SharedPtr<Kernel::Process>& process) override;
ResultStatus ReadRomFS(FileSys::VirtualFile& dir) override; ResultStatus ReadRomFS(FileSys::VirtualFile& dir) override;
u64 ReadRomFSIVFCOffset() const override;
ResultStatus ReadProgramId(u64& out_program_id) override; ResultStatus ReadProgramId(u64& out_program_id) override;
private: private:

@ -232,4 +232,9 @@ ResultStatus AppLoader_NRO::ReadTitle(std::string& title) {
title = nacp->GetApplicationName(); title = nacp->GetApplicationName();
return ResultStatus::Success; return ResultStatus::Success;
} }
bool AppLoader_NRO::IsRomFSUpdatable() const {
return false;
}
} // namespace Loader } // namespace Loader

@ -39,6 +39,7 @@ public:
ResultStatus ReadProgramId(u64& out_program_id) override; ResultStatus ReadProgramId(u64& out_program_id) override;
ResultStatus ReadRomFS(FileSys::VirtualFile& dir) override; ResultStatus ReadRomFS(FileSys::VirtualFile& dir) override;
ResultStatus ReadTitle(std::string& title) override; ResultStatus ReadTitle(std::string& title) override;
bool IsRomFSUpdatable() const override;
private: private:
bool LoadNro(FileSys::VirtualFile file, VAddr load_base); bool LoadNro(FileSys::VirtualFile file, VAddr load_base);

@ -9,6 +9,8 @@
#include "core/file_sys/content_archive.h" #include "core/file_sys/content_archive.h"
#include "core/file_sys/control_metadata.h" #include "core/file_sys/control_metadata.h"
#include "core/file_sys/nca_metadata.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.h" #include "core/file_sys/romfs.h"
#include "core/file_sys/submission_package.h" #include "core/file_sys/submission_package.h"
#include "core/hle/kernel/process.h" #include "core/hle/kernel/process.h"
@ -28,24 +30,12 @@ AppLoader_NSP::AppLoader_NSP(FileSys::VirtualFile file)
return; return;
const auto control_nca = const auto control_nca =
nsp->GetNCA(nsp->GetFirstTitleID(), FileSys::ContentRecordType::Control); nsp->GetNCA(nsp->GetProgramTitleID(), FileSys::ContentRecordType::Control);
if (control_nca == nullptr || control_nca->GetStatus() != ResultStatus::Success) if (control_nca == nullptr || control_nca->GetStatus() != ResultStatus::Success)
return; return;
const auto romfs = FileSys::ExtractRomFS(control_nca->GetRomFS()); std::tie(nacp_file, icon_file) =
if (romfs == nullptr) FileSys::PatchManager(nsp->GetProgramTitleID()).ParseControlNCA(control_nca);
return;
for (const auto& language : FileSys::LANGUAGE_NAMES) {
icon_file = romfs->GetFile("icon_" + std::string(language) + ".dat");
if (icon_file != nullptr)
break;
}
const auto nacp_raw = romfs->GetFile("control.nacp");
if (nacp_raw == nullptr)
return;
nacp_file = std::make_shared<FileSys::NACP>(nacp_raw);
} }
AppLoader_NSP::~AppLoader_NSP() = default; AppLoader_NSP::~AppLoader_NSP() = default;

@ -8,7 +8,9 @@
#include "core/file_sys/card_image.h" #include "core/file_sys/card_image.h"
#include "core/file_sys/content_archive.h" #include "core/file_sys/content_archive.h"
#include "core/file_sys/control_metadata.h" #include "core/file_sys/control_metadata.h"
#include "core/file_sys/patch_manager.h"
#include "core/file_sys/romfs.h" #include "core/file_sys/romfs.h"
#include "core/file_sys/submission_package.h"
#include "core/hle/kernel/process.h" #include "core/hle/kernel/process.h"
#include "core/loader/nca.h" #include "core/loader/nca.h"
#include "core/loader/xci.h" #include "core/loader/xci.h"
@ -20,21 +22,13 @@ AppLoader_XCI::AppLoader_XCI(FileSys::VirtualFile file)
nca_loader(std::make_unique<AppLoader_NCA>(xci->GetProgramNCAFile())) { nca_loader(std::make_unique<AppLoader_NCA>(xci->GetProgramNCAFile())) {
if (xci->GetStatus() != ResultStatus::Success) if (xci->GetStatus() != ResultStatus::Success)
return; return;
const auto control_nca = xci->GetNCAByType(FileSys::NCAContentType::Control); const auto control_nca = xci->GetNCAByType(FileSys::NCAContentType::Control);
if (control_nca == nullptr || control_nca->GetStatus() != ResultStatus::Success) if (control_nca == nullptr || control_nca->GetStatus() != ResultStatus::Success)
return; return;
const auto romfs = FileSys::ExtractRomFS(control_nca->GetRomFS());
if (romfs == nullptr) std::tie(nacp_file, icon_file) =
return; FileSys::PatchManager(xci->GetProgramTitleID()).ParseControlNCA(control_nca);
for (const auto& language : FileSys::LANGUAGE_NAMES) {
icon_file = romfs->GetFile("icon_" + std::string(language) + ".dat");
if (icon_file != nullptr)
break;
}
const auto nacp_raw = romfs->GetFile("control.nacp");
if (nacp_raw == nullptr)
return;
nacp_file = std::make_shared<FileSys::NACP>(nacp_raw);
} }
AppLoader_XCI::~AppLoader_XCI() = default; AppLoader_XCI::~AppLoader_XCI() = default;

@ -7,6 +7,8 @@
#include "common/file_util.h" #include "common/file_util.h"
#include "core/core.h" #include "core/core.h"
#include "core/file_sys/control_metadata.h"
#include "core/file_sys/patch_manager.h"
#include "core/loader/loader.h" #include "core/loader/loader.h"
#include "core/settings.h" #include "core/settings.h"
#include "core/telemetry_session.h" #include "core/telemetry_session.h"
@ -88,12 +90,28 @@ TelemetrySession::TelemetrySession() {
std::chrono::system_clock::now().time_since_epoch()) std::chrono::system_clock::now().time_since_epoch())
.count()}; .count()};
AddField(Telemetry::FieldType::Session, "Init_Time", init_time); AddField(Telemetry::FieldType::Session, "Init_Time", init_time);
std::string program_name;
const Loader::ResultStatus res{System::GetInstance().GetAppLoader().ReadTitle(program_name)}; u64 program_id{};
const Loader::ResultStatus res{System::GetInstance().GetAppLoader().ReadProgramId(program_id)};
if (res == Loader::ResultStatus::Success) { if (res == Loader::ResultStatus::Success) {
AddField(Telemetry::FieldType::Session, "ProgramName", program_name); AddField(Telemetry::FieldType::Session, "ProgramId", program_id);
std::string name;
System::GetInstance().GetAppLoader().ReadTitle(name);
if (name.empty()) {
auto [nacp, icon_file] = FileSys::PatchManager(program_id).GetControlMetadata();
if (nacp != nullptr)
name = nacp->GetApplicationName();
}
if (!name.empty())
AddField(Telemetry::FieldType::Session, "ProgramName", name);
} }
AddField(Telemetry::FieldType::Session, "ProgramFormat",
static_cast<u8>(System::GetInstance().GetAppLoader().GetFileType()));
// Log application information // Log application information
Telemetry::AppendBuildInfo(field_collection); Telemetry::AppendBuildInfo(field_collection);

@ -21,6 +21,7 @@
#include "core/file_sys/content_archive.h" #include "core/file_sys/content_archive.h"
#include "core/file_sys/control_metadata.h" #include "core/file_sys/control_metadata.h"
#include "core/file_sys/nca_metadata.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/registered_cache.h"
#include "core/file_sys/romfs.h" #include "core/file_sys/romfs.h"
#include "core/file_sys/vfs_real.h" #include "core/file_sys/vfs_real.h"
@ -232,6 +233,7 @@ GameList::GameList(FileSys::VirtualFilesystem vfs, GMainWindow* parent)
item_model->insertColumns(0, COLUMN_COUNT); item_model->insertColumns(0, COLUMN_COUNT);
item_model->setHeaderData(COLUMN_NAME, Qt::Horizontal, "Name"); item_model->setHeaderData(COLUMN_NAME, Qt::Horizontal, "Name");
item_model->setHeaderData(COLUMN_COMPATIBILITY, Qt::Horizontal, "Compatibility"); item_model->setHeaderData(COLUMN_COMPATIBILITY, Qt::Horizontal, "Compatibility");
item_model->setHeaderData(COLUMN_ADD_ONS, Qt::Horizontal, "Add-ons");
item_model->setHeaderData(COLUMN_FILE_TYPE, Qt::Horizontal, "File type"); item_model->setHeaderData(COLUMN_FILE_TYPE, Qt::Horizontal, "File type");
item_model->setHeaderData(COLUMN_SIZE, Qt::Horizontal, "Size"); item_model->setHeaderData(COLUMN_SIZE, Qt::Horizontal, "Size");
@ -454,6 +456,25 @@ static QString FormatGameName(const std::string& physical_name) {
return physical_name_as_qstring; return physical_name_as_qstring;
} }
static QString FormatPatchNameVersions(const FileSys::PatchManager& patch_manager,
bool updatable = true) {
QString out;
for (const auto& kv : patch_manager.GetPatchVersionNames()) {
if (!updatable && kv.first == FileSys::PatchType::Update)
continue;
if (kv.second.empty()) {
out.append(fmt::format("{}\n", FileSys::FormatPatchTypeName(kv.first)).c_str());
} else {
out.append(fmt::format("{} ({})\n", FileSys::FormatPatchTypeName(kv.first), kv.second)
.c_str());
}
}
out.chop(1);
return out;
}
void GameList::RefreshGameDirectory() { void GameList::RefreshGameDirectory() {
if (!UISettings::values.gamedir.isEmpty() && current_worker != nullptr) { if (!UISettings::values.gamedir.isEmpty() && current_worker != nullptr) {
LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list."); LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list.");
@ -462,26 +483,14 @@ void GameList::RefreshGameDirectory() {
} }
} }
static void GetMetadataFromControlNCA(const std::shared_ptr<FileSys::NCA>& nca, static void GetMetadataFromControlNCA(const FileSys::PatchManager& patch_manager,
const std::shared_ptr<FileSys::NCA>& nca,
std::vector<u8>& icon, std::string& name) { std::vector<u8>& icon, std::string& name) {
const auto control_dir = FileSys::ExtractRomFS(nca->GetRomFS()); auto [nacp, icon_file] = patch_manager.ParseControlNCA(nca);
if (control_dir == nullptr) if (icon_file != nullptr)
return; icon = icon_file->ReadAllBytes();
if (nacp != nullptr)
const auto nacp_file = control_dir->GetFile("control.nacp"); name = nacp->GetApplicationName();
if (nacp_file == nullptr)
return;
FileSys::NACP nacp(nacp_file);
name = nacp.GetApplicationName();
FileSys::VirtualFile icon_file = nullptr;
for (const auto& language : FileSys::LANGUAGE_NAMES) {
icon_file = control_dir->GetFile("icon_" + std::string(language) + ".dat");
if (icon_file != nullptr) {
icon = icon_file->ReadAllBytes();
break;
}
}
} }
GameListWorker::GameListWorker( GameListWorker::GameListWorker(
@ -492,7 +501,8 @@ GameListWorker::GameListWorker(
GameListWorker::~GameListWorker() = default; GameListWorker::~GameListWorker() = default;
void GameListWorker::AddInstalledTitlesToGameList(std::shared_ptr<FileSys::RegisteredCache> cache) { void GameListWorker::AddInstalledTitlesToGameList() {
const auto cache = Service::FileSystem::GetUnionContents();
const auto installed_games = cache->ListEntriesFilter(FileSys::TitleType::Application, const auto installed_games = cache->ListEntriesFilter(FileSys::TitleType::Application,
FileSys::ContentRecordType::Program); FileSys::ContentRecordType::Program);
@ -507,14 +517,25 @@ void GameListWorker::AddInstalledTitlesToGameList(std::shared_ptr<FileSys::Regis
u64 program_id = 0; u64 program_id = 0;
loader->ReadProgramId(program_id); loader->ReadProgramId(program_id);
const FileSys::PatchManager patch{program_id};
const auto& control = cache->GetEntry(game.title_id, FileSys::ContentRecordType::Control); const auto& control = cache->GetEntry(game.title_id, FileSys::ContentRecordType::Control);
if (control != nullptr) if (control != nullptr)
GetMetadataFromControlNCA(control, icon, name); GetMetadataFromControlNCA(patch, control, icon, name);
auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id);
// The game list uses this as compatibility number for untested games
QString compatibility("99");
if (it != compatibility_list.end())
compatibility = it->second.first;
emit EntryReady({ emit EntryReady({
new GameListItemPath( new GameListItemPath(
FormatGameName(file->GetFullPath()), icon, QString::fromStdString(name), FormatGameName(file->GetFullPath()), icon, QString::fromStdString(name),
QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType())), QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType())),
program_id), program_id),
new GameListItemCompat(compatibility),
new GameListItem(FormatPatchNameVersions(patch)),
new GameListItem( new GameListItem(
QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))), QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))),
new GameListItemSize(file->GetSize()), new GameListItemSize(file->GetSize()),
@ -580,12 +601,14 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign
std::string name = " "; std::string name = " ";
const auto res3 = loader->ReadTitle(name); const auto res3 = loader->ReadTitle(name);
const FileSys::PatchManager patch{program_id};
if (res1 != Loader::ResultStatus::Success && res3 != Loader::ResultStatus::Success && if (res1 != Loader::ResultStatus::Success && res3 != Loader::ResultStatus::Success &&
res2 == Loader::ResultStatus::Success) { res2 == Loader::ResultStatus::Success) {
// Use from metadata pool. // Use from metadata pool.
if (nca_control_map.find(program_id) != nca_control_map.end()) { if (nca_control_map.find(program_id) != nca_control_map.end()) {
const auto nca = nca_control_map[program_id]; const auto nca = nca_control_map[program_id];
GetMetadataFromControlNCA(nca, icon, name); GetMetadataFromControlNCA(patch, nca, icon, name);
} }
} }
@ -602,6 +625,7 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign
QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType())), QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType())),
program_id), program_id),
new GameListItemCompat(compatibility), new GameListItemCompat(compatibility),
new GameListItem(FormatPatchNameVersions(patch, loader->IsRomFSUpdatable())),
new GameListItem( new GameListItem(
QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))), QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))),
new GameListItemSize(FileUtil::GetSize(physical_name)), new GameListItemSize(FileUtil::GetSize(physical_name)),
@ -621,9 +645,7 @@ void GameListWorker::run() {
stop_processing = false; stop_processing = false;
watch_list.append(dir_path); watch_list.append(dir_path);
FillControlMap(dir_path.toStdString()); FillControlMap(dir_path.toStdString());
AddInstalledTitlesToGameList(Service::FileSystem::GetUserNANDContents()); AddInstalledTitlesToGameList();
AddInstalledTitlesToGameList(Service::FileSystem::GetSystemNANDContents());
AddInstalledTitlesToGameList(Service::FileSystem::GetSDMCContents());
AddFstEntriesToGameList(dir_path.toStdString(), deep_scan ? 256 : 0); AddFstEntriesToGameList(dir_path.toStdString(), deep_scan ? 256 : 0);
nca_control_map.clear(); nca_control_map.clear();
emit Finished(watch_list); emit Finished(watch_list);

@ -38,6 +38,7 @@ public:
enum { enum {
COLUMN_NAME, COLUMN_NAME,
COLUMN_COMPATIBILITY, COLUMN_COMPATIBILITY,
COLUMN_ADD_ONS,
COLUMN_FILE_TYPE, COLUMN_FILE_TYPE,
COLUMN_SIZE, COLUMN_SIZE,
COLUMN_COUNT, // Number of columns COLUMN_COUNT, // Number of columns

@ -239,7 +239,7 @@ private:
const std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list; const std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list;
std::atomic_bool stop_processing; std::atomic_bool stop_processing;
void AddInstalledTitlesToGameList(std::shared_ptr<FileSys::RegisteredCache> cache); void AddInstalledTitlesToGameList();
void FillControlMap(const std::string& dir_path); void FillControlMap(const std::string& dir_path);
void AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion = 0); void AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion = 0);
}; };

@ -32,6 +32,8 @@
#include "core/crypto/key_manager.h" #include "core/crypto/key_manager.h"
#include "core/file_sys/card_image.h" #include "core/file_sys/card_image.h"
#include "core/file_sys/content_archive.h" #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/registered_cache.h"
#include "core/file_sys/savedata_factory.h" #include "core/file_sys/savedata_factory.h"
#include "core/file_sys/submission_package.h" #include "core/file_sys/submission_package.h"
@ -592,8 +594,16 @@ void GMainWindow::BootGame(const QString& filename) {
std::string title_name; std::string title_name;
const auto res = Core::System::GetInstance().GetGameName(title_name); const auto res = Core::System::GetInstance().GetGameName(title_name);
if (res != Loader::ResultStatus::Success) if (res != Loader::ResultStatus::Success) {
title_name = FileUtil::GetFilename(filename.toStdString()); const u64 program_id = Core::System::GetInstance().CurrentProcess()->program_id;
const auto [nacp, icon_file] = FileSys::PatchManager(program_id).GetControlMetadata();
if (nacp != nullptr)
title_name = nacp->GetApplicationName();
if (title_name.empty())
title_name = FileUtil::GetFilename(filename.toStdString());
}
setWindowTitle(QString("yuzu %1| %4 | %2-%3") setWindowTitle(QString("yuzu %1| %4 | %2-%3")
.arg(Common::g_build_name, Common::g_scm_branch, Common::g_scm_desc, .arg(Common::g_build_name, Common::g_scm_branch, Common::g_scm_desc,
@ -868,7 +878,11 @@ void GMainWindow::OnMenuInstallToNAND() {
} else { } else {
const auto nca = std::make_shared<FileSys::NCA>( const auto nca = std::make_shared<FileSys::NCA>(
vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read)); vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
if (nca->GetStatus() != Loader::ResultStatus::Success) { const auto id = nca->GetStatus();
// Game updates necessary are missing base RomFS
if (id != Loader::ResultStatus::Success &&
id != Loader::ResultStatus::ErrorMissingBKTRBaseRomFS) {
failed(); failed();
return; return;
} }