Просмотр исходного кода

Merge pull request #1005 from DarkLordZach/registered-fmt

file_sys: Add support for registration format
bunnei 8 лет назад
Родитель
Сommit
c594ec3417

+ 2 - 0
src/common/CMakeLists.txt

@@ -38,6 +38,8 @@ add_library(common STATIC
     file_util.cpp
     file_util.h
     hash.h
+    hex_util.cpp
+    hex_util.h
     logging/backend.cpp
     logging/backend.h
     logging/filter.cpp

+ 6 - 0
src/common/file_util.cpp

@@ -750,6 +750,12 @@ std::string GetHactoolConfigurationPath() {
 #endif
 }
 
+std::string GetNANDRegistrationDir(bool system) {
+    if (system)
+        return GetUserPath(UserPath::NANDDir) + "system/Contents/registered/";
+    return GetUserPath(UserPath::NANDDir) + "user/Contents/registered/";
+}
+
 size_t WriteStringToFile(bool text_file, const std::string& str, const char* filename) {
     return FileUtil::IOFile(filename, text_file ? "w" : "wb").WriteBytes(str.data(), str.size());
 }

+ 2 - 0
src/common/file_util.h

@@ -129,6 +129,8 @@ const std::string& GetUserPath(UserPath path, const std::string& new_path = "");
 
 std::string GetHactoolConfigurationPath();
 
+std::string GetNANDRegistrationDir(bool system = false);
+
 // Returns the path to where the sys file are
 std::string GetSysDirectory();
 

+ 27 - 0
src/common/hex_util.cpp

@@ -0,0 +1,27 @@
+// Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include "common/hex_util.h"
+
+u8 ToHexNibble(char c1) {
+    if (c1 >= 65 && c1 <= 70)
+        return c1 - 55;
+    if (c1 >= 97 && c1 <= 102)
+        return c1 - 87;
+    if (c1 >= 48 && c1 <= 57)
+        return c1 - 48;
+    throw std::logic_error("Invalid hex digit");
+}
+
+std::array<u8, 16> operator""_array16(const char* str, size_t len) {
+    if (len != 32)
+        throw std::logic_error("Not of correct size.");
+    return HexStringToArray<16>(str);
+}
+
+std::array<u8, 32> operator""_array32(const char* str, size_t len) {
+    if (len != 64)
+        throw std::logic_error("Not of correct size.");
+    return HexStringToArray<32>(str);
+}

+ 37 - 0
src/common/hex_util.h

@@ -0,0 +1,37 @@
+// Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <array>
+#include <cstddef>
+#include <string>
+#include <fmt/format.h>
+#include "common/common_types.h"
+
+u8 ToHexNibble(char c1);
+
+template <size_t Size, bool le = false>
+std::array<u8, Size> HexStringToArray(std::string_view str) {
+    std::array<u8, Size> out{};
+    if constexpr (le) {
+        for (size_t i = 2 * Size - 2; i <= 2 * Size; i -= 2)
+            out[i / 2] = (ToHexNibble(str[i]) << 4) | ToHexNibble(str[i + 1]);
+    } else {
+        for (size_t i = 0; i < 2 * Size; i += 2)
+            out[i / 2] = (ToHexNibble(str[i]) << 4) | ToHexNibble(str[i + 1]);
+    }
+    return out;
+}
+
+template <size_t Size>
+std::string HexArrayToString(std::array<u8, Size> array, bool upper = true) {
+    std::string out;
+    for (u8 c : array)
+        out += fmt::format(upper ? "{:02X}" : "{:02x}", c);
+    return out;
+}
+
+std::array<u8, 0x10> operator"" _array16(const char* str, size_t len);
+std::array<u8, 0x20> operator"" _array32(const char* str, size_t len);

+ 8 - 0
src/core/CMakeLists.txt

@@ -20,6 +20,8 @@ add_library(core STATIC
     crypto/key_manager.h
     crypto/ctr_encryption_layer.cpp
     crypto/ctr_encryption_layer.h
+    file_sys/bis_factory.cpp
+    file_sys/bis_factory.h
     file_sys/card_image.cpp
     file_sys/card_image.h
     file_sys/content_archive.cpp
@@ -29,10 +31,14 @@ add_library(core STATIC
     file_sys/directory.h
     file_sys/errors.h
     file_sys/mode.h
+    file_sys/nca_metadata.cpp
+    file_sys/nca_metadata.h
     file_sys/partition_filesystem.cpp
     file_sys/partition_filesystem.h
     file_sys/program_metadata.cpp
     file_sys/program_metadata.h
+    file_sys/registered_cache.cpp
+    file_sys/registered_cache.h
     file_sys/romfs.cpp
     file_sys/romfs.h
     file_sys/romfs_factory.cpp
@@ -43,6 +49,8 @@ add_library(core STATIC
     file_sys/sdmc_factory.h
     file_sys/vfs.cpp
     file_sys/vfs.h
+    file_sys/vfs_concat.cpp
+    file_sys/vfs_concat.h
     file_sys/vfs_offset.cpp
     file_sys/vfs_offset.h
     file_sys/vfs_real.cpp

+ 34 - 1
src/core/core.cpp

@@ -5,6 +5,7 @@
 #include <memory>
 #include <utility>
 #include "common/logging/log.h"
+#include "common/string_util.h"
 #include "core/core.h"
 #include "core/core_timing.h"
 #include "core/gdbstub/gdbstub.h"
@@ -17,6 +18,7 @@
 #include "core/hle/service/sm/sm.h"
 #include "core/loader/loader.h"
 #include "core/settings.h"
+#include "file_sys/vfs_concat.h"
 #include "file_sys/vfs_real.h"
 #include "video_core/renderer_base.h"
 #include "video_core/video_core.h"
@@ -88,8 +90,39 @@ System::ResultStatus System::SingleStep() {
     return RunLoop(false);
 }
 
+static FileSys::VirtualFile GetGameFileFromPath(const FileSys::VirtualFilesystem& vfs,
+                                                const std::string& path) {
+    // To account for split 00+01+etc files.
+    std::string dir_name;
+    std::string filename;
+    Common::SplitPath(path, &dir_name, &filename, nullptr);
+    if (filename == "00") {
+        const auto dir = vfs->OpenDirectory(dir_name, FileSys::Mode::Read);
+        std::vector<FileSys::VirtualFile> concat;
+        for (u8 i = 0; i < 0x10; ++i) {
+            auto next = dir->GetFile(fmt::format("{:02X}", i));
+            if (next != nullptr)
+                concat.push_back(std::move(next));
+            else {
+                next = dir->GetFile(fmt::format("{:02x}", i));
+                if (next != nullptr)
+                    concat.push_back(std::move(next));
+                else
+                    break;
+            }
+        }
+
+        if (concat.empty())
+            return nullptr;
+
+        return FileSys::ConcatenateFiles(concat, dir->GetName());
+    }
+
+    return vfs->OpenFile(path, FileSys::Mode::Read);
+}
+
 System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::string& filepath) {
-    app_loader = Loader::GetLoader(virtual_filesystem->OpenFile(filepath, FileSys::Mode::Read));
+    app_loader = Loader::GetLoader(GetGameFileFromPath(virtual_filesystem, filepath));
 
     if (!app_loader) {
         LOG_CRITICAL(Core, "Failed to obtain loader for {}!", filepath);

+ 2 - 33
src/core/crypto/key_manager.cpp

@@ -10,44 +10,13 @@
 #include <string_view>
 #include "common/common_paths.h"
 #include "common/file_util.h"
+#include "common/hex_util.h"
+#include "common/logging/log.h"
 #include "core/crypto/key_manager.h"
 #include "core/settings.h"
 
 namespace Core::Crypto {
 
-static u8 ToHexNibble(char c1) {
-    if (c1 >= 65 && c1 <= 70)
-        return c1 - 55;
-    if (c1 >= 97 && c1 <= 102)
-        return c1 - 87;
-    if (c1 >= 48 && c1 <= 57)
-        return c1 - 48;
-    throw std::logic_error("Invalid hex digit");
-}
-
-template <size_t Size>
-static std::array<u8, Size> HexStringToArray(std::string_view str) {
-    std::array<u8, Size> out{};
-    for (size_t i = 0; i < 2 * Size; i += 2) {
-        auto d1 = str[i];
-        auto d2 = str[i + 1];
-        out[i / 2] = (ToHexNibble(d1) << 4) | ToHexNibble(d2);
-    }
-    return out;
-}
-
-std::array<u8, 16> operator""_array16(const char* str, size_t len) {
-    if (len != 32)
-        throw std::logic_error("Not of correct size.");
-    return HexStringToArray<16>(str);
-}
-
-std::array<u8, 32> operator""_array32(const char* str, size_t len) {
-    if (len != 64)
-        throw std::logic_error("Not of correct size.");
-    return HexStringToArray<32>(str);
-}
-
 KeyManager::KeyManager() {
     // Initialize keys
     const std::string hactool_keys_dir = FileUtil::GetHactoolConfigurationPath();

+ 0 - 3
src/core/crypto/key_manager.h

@@ -87,9 +87,6 @@ struct hash<Core::Crypto::KeyIndex<KeyType>> {
 
 namespace Core::Crypto {
 
-std::array<u8, 0x10> operator"" _array16(const char* str, size_t len);
-std::array<u8, 0x20> operator"" _array32(const char* str, size_t len);
-
 class KeyManager {
 public:
     KeyManager();

+ 31 - 0
src/core/file_sys/bis_factory.cpp

@@ -0,0 +1,31 @@
+// Copyright 2018 yuzu emulator team
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include "core/file_sys/bis_factory.h"
+
+namespace FileSys {
+
+static VirtualDir GetOrCreateDirectory(const VirtualDir& dir, std::string_view path) {
+    const auto res = dir->GetDirectoryRelative(path);
+    if (res == nullptr)
+        return dir->CreateDirectoryRelative(path);
+    return res;
+}
+
+BISFactory::BISFactory(VirtualDir nand_root_)
+    : nand_root(std::move(nand_root_)),
+      sysnand_cache(std::make_shared<RegisteredCache>(
+          GetOrCreateDirectory(nand_root, "/system/Contents/registered"))),
+      usrnand_cache(std::make_shared<RegisteredCache>(
+          GetOrCreateDirectory(nand_root, "/user/Contents/registered"))) {}
+
+std::shared_ptr<RegisteredCache> BISFactory::GetSystemNANDContents() const {
+    return sysnand_cache;
+}
+
+std::shared_ptr<RegisteredCache> BISFactory::GetUserNANDContents() const {
+    return usrnand_cache;
+}
+
+} // namespace FileSys

+ 30 - 0
src/core/file_sys/bis_factory.h

@@ -0,0 +1,30 @@
+// Copyright 2018 yuzu emulator team
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <memory>
+#include "core/loader/loader.h"
+#include "registered_cache.h"
+
+namespace FileSys {
+
+/// File system interface to the Built-In Storage
+/// This is currently missing accessors to BIS partitions, but seemed like a good place for the NAND
+/// registered caches.
+class BISFactory {
+public:
+    explicit BISFactory(VirtualDir nand_root);
+
+    std::shared_ptr<RegisteredCache> GetSystemNANDContents() const;
+    std::shared_ptr<RegisteredCache> GetUserNANDContents() const;
+
+private:
+    VirtualDir nand_root;
+
+    std::shared_ptr<RegisteredCache> sysnand_cache;
+    std::shared_ptr<RegisteredCache> usrnand_cache;
+};
+
+} // namespace FileSys

+ 4 - 0
src/core/file_sys/card_image.cpp

@@ -96,6 +96,10 @@ VirtualDir XCI::GetLogoPartition() const {
     return GetPartition(XCIPartition::Logo);
 }
 
+const std::vector<std::shared_ptr<NCA>>& XCI::GetNCAs() const {
+    return ncas;
+}
+
 std::shared_ptr<NCA> XCI::GetNCAByType(NCAContentType type) const {
     const auto iter =
         std::find_if(ncas.begin(), ncas.end(),

+ 1 - 0
src/core/file_sys/card_image.h

@@ -68,6 +68,7 @@ public:
     VirtualDir GetUpdatePartition() const;
     VirtualDir GetLogoPartition() const;
 
+    const std::vector<std::shared_ptr<NCA>>& GetNCAs() const;
     std::shared_ptr<NCA> GetNCAByType(NCAContentType type) const;
     VirtualFile GetNCAFileByType(NCAContentType type) const;
 

+ 1 - 1
src/core/file_sys/control_metadata.cpp

@@ -16,7 +16,7 @@ std::string LanguageEntry::GetDeveloperName() const {
     return Common::StringFromFixedZeroTerminatedBuffer(developer_name.data(), 0x100);
 }
 
-NACP::NACP(VirtualFile file_) : file(std::move(file_)), raw(std::make_unique<RawNACP>()) {
+NACP::NACP(VirtualFile file) : raw(std::make_unique<RawNACP>()) {
     file->ReadObject(raw.get());
 }
 

+ 0 - 1
src/core/file_sys/control_metadata.h

@@ -81,7 +81,6 @@ public:
     std::string GetVersionString() const;
 
 private:
-    VirtualFile file;
     std::unique_ptr<RawNACP> raw;
 };
 

+ 131 - 0
src/core/file_sys/nca_metadata.cpp

@@ -0,0 +1,131 @@
+// Copyright 2018 yuzu emulator team
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <cstring>
+#include "common/common_funcs.h"
+#include "common/logging/log.h"
+#include "common/swap.h"
+#include "content_archive.h"
+#include "core/file_sys/nca_metadata.h"
+
+namespace FileSys {
+
+bool operator>=(TitleType lhs, TitleType rhs) {
+    return static_cast<size_t>(lhs) >= static_cast<size_t>(rhs);
+}
+
+bool operator<=(TitleType lhs, TitleType rhs) {
+    return static_cast<size_t>(lhs) <= static_cast<size_t>(rhs);
+}
+
+CNMT::CNMT(VirtualFile file) {
+    if (file->ReadObject(&header) != sizeof(CNMTHeader))
+        return;
+
+    // If type is {Application, Update, AOC} has opt-header.
+    if (header.type >= TitleType::Application && header.type <= TitleType::AOC) {
+        if (file->ReadObject(&opt_header, sizeof(CNMTHeader)) != sizeof(OptionalHeader)) {
+            LOG_WARNING(Loader, "Failed to read optional header.");
+        }
+    }
+
+    for (u16 i = 0; i < header.number_content_entries; ++i) {
+        auto& next = content_records.emplace_back(ContentRecord{});
+        if (file->ReadObject(&next, sizeof(CNMTHeader) + i * sizeof(ContentRecord) +
+                                        header.table_offset) != sizeof(ContentRecord)) {
+            content_records.erase(content_records.end() - 1);
+        }
+    }
+
+    for (u16 i = 0; i < header.number_meta_entries; ++i) {
+        auto& next = meta_records.emplace_back(MetaRecord{});
+        if (file->ReadObject(&next, sizeof(CNMTHeader) + i * sizeof(MetaRecord) +
+                                        header.table_offset) != sizeof(MetaRecord)) {
+            meta_records.erase(meta_records.end() - 1);
+        }
+    }
+}
+
+CNMT::CNMT(CNMTHeader header, OptionalHeader opt_header, std::vector<ContentRecord> content_records,
+           std::vector<MetaRecord> meta_records)
+    : header(std::move(header)), opt_header(std::move(opt_header)),
+      content_records(std::move(content_records)), meta_records(std::move(meta_records)) {}
+
+u64 CNMT::GetTitleID() const {
+    return header.title_id;
+}
+
+u32 CNMT::GetTitleVersion() const {
+    return header.title_version;
+}
+
+TitleType CNMT::GetType() const {
+    return header.type;
+}
+
+const std::vector<ContentRecord>& CNMT::GetContentRecords() const {
+    return content_records;
+}
+
+const std::vector<MetaRecord>& CNMT::GetMetaRecords() const {
+    return meta_records;
+}
+
+bool CNMT::UnionRecords(const CNMT& other) {
+    bool change = false;
+    for (const auto& rec : other.content_records) {
+        const auto iter = std::find_if(content_records.begin(), content_records.end(),
+                                       [&rec](const ContentRecord& r) {
+                                           return r.nca_id == rec.nca_id && r.type == rec.type;
+                                       });
+        if (iter == content_records.end()) {
+            content_records.emplace_back(rec);
+            ++header.number_content_entries;
+            change = true;
+        }
+    }
+    for (const auto& rec : other.meta_records) {
+        const auto iter =
+            std::find_if(meta_records.begin(), meta_records.end(), [&rec](const MetaRecord& r) {
+                return r.title_id == rec.title_id && r.title_version == rec.title_version &&
+                       r.type == rec.type;
+            });
+        if (iter == meta_records.end()) {
+            meta_records.emplace_back(rec);
+            ++header.number_meta_entries;
+            change = true;
+        }
+    }
+    return change;
+}
+
+std::vector<u8> CNMT::Serialize() const {
+    const bool has_opt_header =
+        header.type >= TitleType::Application && header.type <= TitleType::AOC;
+    const auto dead_zone = header.table_offset + sizeof(CNMTHeader);
+    std::vector<u8> out(
+        std::max(sizeof(CNMTHeader) + (has_opt_header ? sizeof(OptionalHeader) : 0), dead_zone) +
+        content_records.size() * sizeof(ContentRecord) + meta_records.size() * sizeof(MetaRecord));
+    memcpy(out.data(), &header, sizeof(CNMTHeader));
+
+    // Optional Header
+    if (has_opt_header) {
+        memcpy(out.data() + sizeof(CNMTHeader), &opt_header, sizeof(OptionalHeader));
+    }
+
+    auto offset = header.table_offset;
+
+    for (const auto& rec : content_records) {
+        memcpy(out.data() + offset + sizeof(CNMTHeader), &rec, sizeof(ContentRecord));
+        offset += sizeof(ContentRecord);
+    }
+
+    for (const auto& rec : meta_records) {
+        memcpy(out.data() + offset + sizeof(CNMTHeader), &rec, sizeof(MetaRecord));
+        offset += sizeof(MetaRecord);
+    }
+
+    return out;
+}
+} // namespace FileSys

+ 111 - 0
src/core/file_sys/nca_metadata.h

@@ -0,0 +1,111 @@
+// Copyright 2018 yuzu emulator team
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <cstring>
+#include <memory>
+#include <vector>
+#include "common/common_types.h"
+#include "common/swap.h"
+#include "core/file_sys/vfs.h"
+
+namespace FileSys {
+class CNMT;
+
+struct CNMTHeader;
+struct OptionalHeader;
+
+enum class TitleType : u8 {
+    SystemProgram = 0x01,
+    SystemDataArchive = 0x02,
+    SystemUpdate = 0x03,
+    FirmwarePackageA = 0x04,
+    FirmwarePackageB = 0x05,
+    Application = 0x80,
+    Update = 0x81,
+    AOC = 0x82,
+    DeltaTitle = 0x83,
+};
+
+bool operator>=(TitleType lhs, TitleType rhs);
+bool operator<=(TitleType lhs, TitleType rhs);
+
+enum class ContentRecordType : u8 {
+    Meta = 0,
+    Program = 1,
+    Data = 2,
+    Control = 3,
+    Manual = 4,
+    Legal = 5,
+    Patch = 6,
+};
+
+struct ContentRecord {
+    std::array<u8, 0x20> hash;
+    std::array<u8, 0x10> nca_id;
+    std::array<u8, 0x6> size;
+    ContentRecordType type;
+    INSERT_PADDING_BYTES(1);
+};
+static_assert(sizeof(ContentRecord) == 0x38, "ContentRecord has incorrect size.");
+
+constexpr ContentRecord EMPTY_META_CONTENT_RECORD{{}, {}, {}, ContentRecordType::Meta, {}};
+
+struct MetaRecord {
+    u64_le title_id;
+    u32_le title_version;
+    TitleType type;
+    u8 install_byte;
+    INSERT_PADDING_BYTES(2);
+};
+static_assert(sizeof(MetaRecord) == 0x10, "MetaRecord has incorrect size.");
+
+struct OptionalHeader {
+    u64_le title_id;
+    u64_le minimum_version;
+};
+static_assert(sizeof(OptionalHeader) == 0x10, "OptionalHeader has incorrect size.");
+
+struct CNMTHeader {
+    u64_le title_id;
+    u32_le title_version;
+    TitleType type;
+    INSERT_PADDING_BYTES(1);
+    u16_le table_offset;
+    u16_le number_content_entries;
+    u16_le number_meta_entries;
+    INSERT_PADDING_BYTES(12);
+};
+static_assert(sizeof(CNMTHeader) == 0x20, "CNMTHeader has incorrect size.");
+
+// A class representing the format used by NCA metadata files, typically named {}.cnmt.nca or
+// meta0.ncd. These describe which NCA's belong with which titles in the registered cache.
+class CNMT {
+public:
+    explicit CNMT(VirtualFile file);
+    CNMT(CNMTHeader header, OptionalHeader opt_header, std::vector<ContentRecord> content_records,
+         std::vector<MetaRecord> meta_records);
+
+    u64 GetTitleID() const;
+    u32 GetTitleVersion() const;
+    TitleType GetType() const;
+
+    const std::vector<ContentRecord>& GetContentRecords() const;
+    const std::vector<MetaRecord>& GetMetaRecords() const;
+
+    bool UnionRecords(const CNMT& other);
+    std::vector<u8> Serialize() const;
+
+private:
+    CNMTHeader header;
+    OptionalHeader opt_header;
+    std::vector<ContentRecord> content_records;
+    std::vector<MetaRecord> meta_records;
+
+    // TODO(DarkLordZach): According to switchbrew, for Patch-type there is additional data
+    // after the table. This is not documented, unfortunately.
+};
+
+} // namespace FileSys

+ 476 - 0
src/core/file_sys/registered_cache.cpp

@@ -0,0 +1,476 @@
+// Copyright 2018 yuzu emulator team
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <regex>
+#include <mbedtls/sha256.h>
+#include "common/assert.h"
+#include "common/hex_util.h"
+#include "common/logging/log.h"
+#include "core/crypto/encryption_layer.h"
+#include "core/file_sys/card_image.h"
+#include "core/file_sys/nca_metadata.h"
+#include "core/file_sys/registered_cache.h"
+#include "core/file_sys/vfs_concat.h"
+
+namespace FileSys {
+std::string RegisteredCacheEntry::DebugInfo() const {
+    return fmt::format("title_id={:016X}, content_type={:02X}", title_id, static_cast<u8>(type));
+}
+
+bool operator<(const RegisteredCacheEntry& lhs, const RegisteredCacheEntry& rhs) {
+    return (lhs.title_id < rhs.title_id) || (lhs.title_id == rhs.title_id && lhs.type < rhs.type);
+}
+
+static bool FollowsTwoDigitDirFormat(std::string_view name) {
+    static const std::regex two_digit_regex("000000[0-9A-F]{2}", std::regex_constants::ECMAScript |
+                                                                     std::regex_constants::icase);
+    return std::regex_match(name.begin(), name.end(), two_digit_regex);
+}
+
+static bool FollowsNcaIdFormat(std::string_view name) {
+    static const std::regex nca_id_regex("[0-9A-F]{32}\\.nca", std::regex_constants::ECMAScript |
+                                                                   std::regex_constants::icase);
+    return name.size() == 36 && std::regex_match(name.begin(), name.end(), nca_id_regex);
+}
+
+static std::string GetRelativePathFromNcaID(const std::array<u8, 16>& nca_id, bool second_hex_upper,
+                                            bool within_two_digit) {
+    if (!within_two_digit)
+        return fmt::format("/{}.nca", HexArrayToString(nca_id, second_hex_upper));
+
+    Core::Crypto::SHA256Hash hash{};
+    mbedtls_sha256(nca_id.data(), nca_id.size(), hash.data(), 0);
+    return fmt::format("/000000{:02X}/{}.nca", hash[0], HexArrayToString(nca_id, second_hex_upper));
+}
+
+static std::string GetCNMTName(TitleType type, u64 title_id) {
+    constexpr std::array<const char*, 9> TITLE_TYPE_NAMES{
+        "SystemProgram",
+        "SystemData",
+        "SystemUpdate",
+        "BootImagePackage",
+        "BootImagePackageSafe",
+        "Application",
+        "Patch",
+        "AddOnContent",
+        "" ///< Currently unknown 'DeltaTitle'
+    };
+
+    auto index = static_cast<size_t>(type);
+    // If the index is after the jump in TitleType, subtract it out.
+    if (index >= static_cast<size_t>(TitleType::Application)) {
+        index -= static_cast<size_t>(TitleType::Application) -
+                 static_cast<size_t>(TitleType::FirmwarePackageB);
+    }
+    return fmt::format("{}_{:016x}.cnmt", TITLE_TYPE_NAMES[index], title_id);
+}
+
+static ContentRecordType GetCRTypeFromNCAType(NCAContentType type) {
+    switch (type) {
+    case NCAContentType::Program:
+        // TODO(DarkLordZach): Differentiate between Program and Patch
+        return ContentRecordType::Program;
+    case NCAContentType::Meta:
+        return ContentRecordType::Meta;
+    case NCAContentType::Control:
+        return ContentRecordType::Control;
+    case NCAContentType::Data:
+        return ContentRecordType::Data;
+    case NCAContentType::Manual:
+        // TODO(DarkLordZach): Peek at NCA contents to differentiate Manual and Legal.
+        return ContentRecordType::Manual;
+    default:
+        UNREACHABLE();
+    }
+}
+
+VirtualFile RegisteredCache::OpenFileOrDirectoryConcat(const VirtualDir& dir,
+                                                       std::string_view path) const {
+    if (dir->GetFileRelative(path) != nullptr)
+        return dir->GetFileRelative(path);
+    if (dir->GetDirectoryRelative(path) != nullptr) {
+        const auto nca_dir = dir->GetDirectoryRelative(path);
+        VirtualFile file = nullptr;
+
+        const auto files = nca_dir->GetFiles();
+        if (files.size() == 1 && files[0]->GetName() == "00") {
+            file = files[0];
+        } else {
+            std::vector<VirtualFile> concat;
+            // Since the files are a two-digit hex number, max is FF.
+            for (size_t i = 0; i < 0x100; ++i) {
+                auto next = nca_dir->GetFile(fmt::format("{:02X}", i));
+                if (next != nullptr) {
+                    concat.push_back(std::move(next));
+                } else {
+                    next = nca_dir->GetFile(fmt::format("{:02x}", i));
+                    if (next != nullptr)
+                        concat.push_back(std::move(next));
+                    else
+                        break;
+                }
+            }
+
+            if (concat.empty())
+                return nullptr;
+
+            file = FileSys::ConcatenateFiles(concat);
+        }
+
+        return file;
+    }
+    return nullptr;
+}
+
+VirtualFile RegisteredCache::GetFileAtID(NcaID id) const {
+    VirtualFile file;
+    // Try all four modes of file storage:
+    // (bit 1 = uppercase/lower, bit 0 = within a two-digit dir)
+    // 00: /000000**/{:032X}.nca
+    // 01: /{:032X}.nca
+    // 10: /000000**/{:032x}.nca
+    // 11: /{:032x}.nca
+    for (u8 i = 0; i < 4; ++i) {
+        const auto path = GetRelativePathFromNcaID(id, (i & 0b10) == 0, (i & 0b01) == 0);
+        file = OpenFileOrDirectoryConcat(dir, path);
+        if (file != nullptr)
+            return file;
+    }
+    return file;
+}
+
+static boost::optional<NcaID> CheckMapForContentRecord(
+    const boost::container::flat_map<u64, CNMT>& map, u64 title_id, ContentRecordType type) {
+    if (map.find(title_id) == map.end())
+        return boost::none;
+
+    const auto& cnmt = map.at(title_id);
+
+    const auto iter = std::find_if(cnmt.GetContentRecords().begin(), cnmt.GetContentRecords().end(),
+                                   [type](const ContentRecord& rec) { return rec.type == type; });
+    if (iter == cnmt.GetContentRecords().end())
+        return boost::none;
+
+    return boost::make_optional(iter->nca_id);
+}
+
+boost::optional<NcaID> RegisteredCache::GetNcaIDFromMetadata(u64 title_id,
+                                                             ContentRecordType type) const {
+    if (type == ContentRecordType::Meta && meta_id.find(title_id) != meta_id.end())
+        return meta_id.at(title_id);
+
+    const auto res1 = CheckMapForContentRecord(yuzu_meta, title_id, type);
+    if (res1 != boost::none)
+        return res1;
+    return CheckMapForContentRecord(meta, title_id, type);
+}
+
+std::vector<NcaID> RegisteredCache::AccumulateFiles() const {
+    std::vector<NcaID> ids;
+    for (const auto& d2_dir : dir->GetSubdirectories()) {
+        if (FollowsNcaIdFormat(d2_dir->GetName())) {
+            ids.push_back(HexStringToArray<0x10, true>(d2_dir->GetName().substr(0, 0x20)));
+            continue;
+        }
+
+        if (!FollowsTwoDigitDirFormat(d2_dir->GetName()))
+            continue;
+
+        for (const auto& nca_dir : d2_dir->GetSubdirectories()) {
+            if (!FollowsNcaIdFormat(nca_dir->GetName()))
+                continue;
+
+            ids.push_back(HexStringToArray<0x10, true>(nca_dir->GetName().substr(0, 0x20)));
+        }
+
+        for (const auto& nca_file : d2_dir->GetFiles()) {
+            if (!FollowsNcaIdFormat(nca_file->GetName()))
+                continue;
+
+            ids.push_back(HexStringToArray<0x10, true>(nca_file->GetName().substr(0, 0x20)));
+        }
+    }
+
+    for (const auto& d2_file : dir->GetFiles()) {
+        if (FollowsNcaIdFormat(d2_file->GetName()))
+            ids.push_back(HexStringToArray<0x10, true>(d2_file->GetName().substr(0, 0x20)));
+    }
+    return ids;
+}
+
+void RegisteredCache::ProcessFiles(const std::vector<NcaID>& ids) {
+    for (const auto& id : ids) {
+        const auto file = GetFileAtID(id);
+
+        if (file == nullptr)
+            continue;
+        const auto nca = std::make_shared<NCA>(parser(file, id));
+        if (nca->GetStatus() != Loader::ResultStatus::Success ||
+            nca->GetType() != NCAContentType::Meta) {
+            continue;
+        }
+
+        const auto section0 = nca->GetSubdirectories()[0];
+
+        for (const auto& file : section0->GetFiles()) {
+            if (file->GetExtension() != "cnmt")
+                continue;
+
+            meta.insert_or_assign(nca->GetTitleId(), CNMT(file));
+            meta_id.insert_or_assign(nca->GetTitleId(), id);
+            break;
+        }
+    }
+}
+
+void RegisteredCache::AccumulateYuzuMeta() {
+    const auto dir = this->dir->GetSubdirectory("yuzu_meta");
+    if (dir == nullptr)
+        return;
+
+    for (const auto& file : dir->GetFiles()) {
+        if (file->GetExtension() != "cnmt")
+            continue;
+
+        CNMT cnmt(file);
+        yuzu_meta.insert_or_assign(cnmt.GetTitleID(), std::move(cnmt));
+    }
+}
+
+void RegisteredCache::Refresh() {
+    if (dir == nullptr)
+        return;
+    const auto ids = AccumulateFiles();
+    ProcessFiles(ids);
+    AccumulateYuzuMeta();
+}
+
+RegisteredCache::RegisteredCache(VirtualDir dir_, RegisteredCacheParsingFunction parsing_function)
+    : dir(std::move(dir_)), parser(std::move(parsing_function)) {
+    Refresh();
+}
+
+bool RegisteredCache::HasEntry(u64 title_id, ContentRecordType type) const {
+    return GetEntryRaw(title_id, type) != nullptr;
+}
+
+bool RegisteredCache::HasEntry(RegisteredCacheEntry entry) const {
+    return GetEntryRaw(entry) != nullptr;
+}
+
+VirtualFile RegisteredCache::GetEntryRaw(u64 title_id, ContentRecordType type) const {
+    const auto id = GetNcaIDFromMetadata(title_id, type);
+    if (id == boost::none)
+        return nullptr;
+
+    return parser(GetFileAtID(id.get()), id.get());
+}
+
+VirtualFile RegisteredCache::GetEntryRaw(RegisteredCacheEntry entry) const {
+    return GetEntryRaw(entry.title_id, entry.type);
+}
+
+std::shared_ptr<NCA> RegisteredCache::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> RegisteredCache::GetEntry(RegisteredCacheEntry entry) const {
+    return GetEntry(entry.title_id, entry.type);
+}
+
+template <typename T>
+void RegisteredCache::IterateAllMetadata(
+    std::vector<T>& out, std::function<T(const CNMT&, const ContentRecord&)> proc,
+    std::function<bool(const CNMT&, const ContentRecord&)> filter) const {
+    for (const auto& kv : meta) {
+        const auto& cnmt = kv.second;
+        if (filter(cnmt, EMPTY_META_CONTENT_RECORD))
+            out.push_back(proc(cnmt, EMPTY_META_CONTENT_RECORD));
+        for (const auto& rec : cnmt.GetContentRecords()) {
+            if (GetFileAtID(rec.nca_id) != nullptr && filter(cnmt, rec)) {
+                out.push_back(proc(cnmt, rec));
+            }
+        }
+    }
+    for (const auto& kv : yuzu_meta) {
+        const auto& cnmt = kv.second;
+        for (const auto& rec : cnmt.GetContentRecords()) {
+            if (GetFileAtID(rec.nca_id) != nullptr && filter(cnmt, rec)) {
+                out.push_back(proc(cnmt, rec));
+            }
+        }
+    }
+}
+
+std::vector<RegisteredCacheEntry> RegisteredCache::ListEntries() const {
+    std::vector<RegisteredCacheEntry> out;
+    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> RegisteredCache::ListEntriesFilter(
+    boost::optional<TitleType> title_type, boost::optional<ContentRecordType> record_type,
+    boost::optional<u64> title_id) const {
+    std::vector<RegisteredCacheEntry> out;
+    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;
+}
+
+static std::shared_ptr<NCA> GetNCAFromXCIForID(std::shared_ptr<XCI> xci, const NcaID& id) {
+    const auto filename = fmt::format("{}.nca", HexArrayToString(id, false));
+    const auto iter =
+        std::find_if(xci->GetNCAs().begin(), xci->GetNCAs().end(),
+                     [&filename](std::shared_ptr<NCA> nca) { return nca->GetName() == filename; });
+    return iter == xci->GetNCAs().end() ? nullptr : *iter;
+}
+
+InstallResult RegisteredCache::InstallEntry(std::shared_ptr<XCI> xci, bool overwrite_if_exists,
+                                            const VfsCopyFunction& copy) {
+    const auto& ncas = xci->GetNCAs();
+    const auto& meta_iter = std::find_if(ncas.begin(), ncas.end(), [](std::shared_ptr<NCA> nca) {
+        return nca->GetType() == NCAContentType::Meta;
+    });
+
+    if (meta_iter == ncas.end()) {
+        LOG_ERROR(Loader, "The XCI you are attempting to install does not have a metadata NCA and "
+                          "is therefore malformed. Double check your encryption keys.");
+        return InstallResult::ErrorMetaFailed;
+    }
+
+    // Install Metadata File
+    const auto meta_id_raw = (*meta_iter)->GetName().substr(0, 32);
+    const auto meta_id = HexStringToArray<16>(meta_id_raw);
+
+    const auto res = RawInstallNCA(*meta_iter, copy, overwrite_if_exists, meta_id);
+    if (res != InstallResult::Success)
+        return res;
+
+    // Install all the other NCAs
+    const auto section0 = (*meta_iter)->GetSubdirectories()[0];
+    const auto cnmt_file = section0->GetFiles()[0];
+    const CNMT cnmt(cnmt_file);
+    for (const auto& record : cnmt.GetContentRecords()) {
+        const auto nca = GetNCAFromXCIForID(xci, record.nca_id);
+        if (nca == nullptr)
+            return InstallResult::ErrorCopyFailed;
+        const auto res2 = RawInstallNCA(nca, copy, overwrite_if_exists, record.nca_id);
+        if (res2 != InstallResult::Success)
+            return res2;
+    }
+
+    Refresh();
+    return InstallResult::Success;
+}
+
+InstallResult RegisteredCache::InstallEntry(std::shared_ptr<NCA> nca, TitleType type,
+                                            bool overwrite_if_exists, const VfsCopyFunction& copy) {
+    CNMTHeader header{
+        nca->GetTitleId(), ///< Title ID
+        0,                 ///< Ignore/Default title version
+        type,              ///< Type
+        {},                ///< Padding
+        0x10,              ///< Default table offset
+        1,                 ///< 1 Content Entry
+        0,                 ///< No Meta Entries
+        {},                ///< Padding
+    };
+    OptionalHeader opt_header{0, 0};
+    ContentRecord c_rec{{}, {}, {}, GetCRTypeFromNCAType(nca->GetType()), {}};
+    const auto& data = nca->GetBaseFile()->ReadBytes(0x100000);
+    mbedtls_sha256(data.data(), data.size(), c_rec.hash.data(), 0);
+    memcpy(&c_rec.nca_id, &c_rec.hash, 16);
+    const CNMT new_cnmt(header, opt_header, {c_rec}, {});
+    if (!RawInstallYuzuMeta(new_cnmt))
+        return InstallResult::ErrorMetaFailed;
+    return RawInstallNCA(nca, copy, overwrite_if_exists, c_rec.nca_id);
+}
+
+InstallResult RegisteredCache::RawInstallNCA(std::shared_ptr<NCA> nca, const VfsCopyFunction& copy,
+                                             bool overwrite_if_exists,
+                                             boost::optional<NcaID> override_id) {
+    const auto in = nca->GetBaseFile();
+    Core::Crypto::SHA256Hash hash{};
+
+    // Calculate NcaID
+    // NOTE: Because computing the SHA256 of an entire NCA is quite expensive (especially if the
+    // game is massive), we're going to cheat and only hash the first MB of the NCA.
+    // Also, for XCIs the NcaID matters, so if the override id isn't none, use that.
+    NcaID id{};
+    if (override_id == boost::none) {
+        const auto& data = in->ReadBytes(0x100000);
+        mbedtls_sha256(data.data(), data.size(), hash.data(), 0);
+        memcpy(id.data(), hash.data(), 16);
+    } else {
+        id = override_id.get();
+    }
+
+    std::string path = GetRelativePathFromNcaID(id, false, true);
+
+    if (GetFileAtID(id) != nullptr && !overwrite_if_exists) {
+        LOG_WARNING(Loader, "Attempting to overwrite existing NCA. Skipping...");
+        return InstallResult::ErrorAlreadyExists;
+    }
+
+    if (GetFileAtID(id) != nullptr) {
+        LOG_WARNING(Loader, "Overwriting existing NCA...");
+        VirtualDir c_dir;
+        { c_dir = dir->GetFileRelative(path)->GetContainingDirectory(); }
+        c_dir->DeleteFile(FileUtil::GetFilename(path));
+    }
+
+    auto out = dir->CreateFileRelative(path);
+    if (out == nullptr)
+        return InstallResult::ErrorCopyFailed;
+    return copy(in, out) ? InstallResult::Success : InstallResult::ErrorCopyFailed;
+}
+
+bool RegisteredCache::RawInstallYuzuMeta(const CNMT& cnmt) {
+    // Reasoning behind this method can be found in the comment for InstallEntry, NCA overload.
+    const auto dir = this->dir->CreateDirectoryRelative("yuzu_meta");
+    const auto filename = GetCNMTName(cnmt.GetType(), cnmt.GetTitleID());
+    if (dir->GetFile(filename) == nullptr) {
+        auto out = dir->CreateFile(filename);
+        const auto buffer = cnmt.Serialize();
+        out->Resize(buffer.size());
+        out->WriteBytes(buffer);
+    } else {
+        auto out = dir->GetFile(filename);
+        CNMT old_cnmt(out);
+        // Returns true on change
+        if (old_cnmt.UnionRecords(cnmt)) {
+            out->Resize(0);
+            const auto buffer = old_cnmt.Serialize();
+            out->Resize(buffer.size());
+            out->WriteBytes(buffer);
+        }
+    }
+    Refresh();
+    return std::find_if(yuzu_meta.begin(), yuzu_meta.end(),
+                        [&cnmt](const std::pair<u64, CNMT>& kv) {
+                            return kv.second.GetType() == cnmt.GetType() &&
+                                   kv.second.GetTitleID() == cnmt.GetTitleID();
+                        }) != yuzu_meta.end();
+}
+} // namespace FileSys

+ 124 - 0
src/core/file_sys/registered_cache.h

@@ -0,0 +1,124 @@
+// Copyright 2018 yuzu emulator team
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <array>
+#include <functional>
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+#include <boost/container/flat_map.hpp>
+#include "common/common_funcs.h"
+#include "common/common_types.h"
+#include "content_archive.h"
+#include "core/file_sys/nca_metadata.h"
+#include "core/file_sys/vfs.h"
+
+namespace FileSys {
+class XCI;
+class CNMT;
+
+using NcaID = std::array<u8, 0x10>;
+using RegisteredCacheParsingFunction = std::function<VirtualFile(const VirtualFile&, const NcaID&)>;
+using VfsCopyFunction = std::function<bool(VirtualFile, VirtualFile)>;
+
+enum class InstallResult {
+    Success,
+    ErrorAlreadyExists,
+    ErrorCopyFailed,
+    ErrorMetaFailed,
+};
+
+struct RegisteredCacheEntry {
+    u64 title_id;
+    ContentRecordType type;
+
+    std::string DebugInfo() const;
+};
+
+// boost flat_map requires operator< for O(log(n)) lookups.
+bool operator<(const RegisteredCacheEntry& lhs, const RegisteredCacheEntry& rhs);
+
+/*
+ * A class that catalogues NCAs in the registered directory structure.
+ * Nintendo's registered format follows this structure:
+ *
+ * Root
+ *   | 000000XX <- XX is the ____ two digits of the NcaID
+ *       | <hash>.nca <- hash is the NcaID (first half of SHA256 over entire file) (folder)
+ *         | 00
+ *         | 01 <- Actual content split along 4GB boundaries. (optional)
+ *
+ * (This impl also supports substituting the nca dir for an nca file, as that's more convenient when
+ * 4GB splitting can be ignored.)
+ */
+class RegisteredCache {
+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
+    // parsing function.
+    explicit RegisteredCache(VirtualDir dir,
+                             RegisteredCacheParsingFunction parsing_function =
+                                 [](const VirtualFile& file, const NcaID& id) { return file; });
+
+    void Refresh();
+
+    bool HasEntry(u64 title_id, ContentRecordType type) const;
+    bool HasEntry(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;
+
+    // Raw copies all the ncas from the xci to the csache. Does some quick checks to make sure there
+    // is a meta NCA and all of them are accessible.
+    InstallResult InstallEntry(std::shared_ptr<XCI> xci, bool overwrite_if_exists = false,
+                               const VfsCopyFunction& copy = &VfsRawCopy);
+
+    // Due to the fact that we must use Meta-type NCAs to determine the existance of files, this
+    // poses quite a challenge. Instead of creating a new meta NCA for this file, yuzu will create a
+    // dir inside the NAND called 'yuzu_meta' and store the raw CNMT there.
+    // TODO(DarkLordZach): Author real meta-type NCAs and install those.
+    InstallResult InstallEntry(std::shared_ptr<NCA> nca, TitleType type,
+                               bool overwrite_if_exists = false,
+                               const VfsCopyFunction& copy = &VfsRawCopy);
+
+private:
+    template <typename T>
+    void IterateAllMetadata(std::vector<T>& out,
+                            std::function<T(const CNMT&, const ContentRecord&)> proc,
+                            std::function<bool(const CNMT&, const ContentRecord&)> filter) const;
+    std::vector<NcaID> AccumulateFiles() const;
+    void ProcessFiles(const std::vector<NcaID>& ids);
+    void AccumulateYuzuMeta();
+    boost::optional<NcaID> GetNcaIDFromMetadata(u64 title_id, ContentRecordType type) const;
+    VirtualFile GetFileAtID(NcaID id) const;
+    VirtualFile OpenFileOrDirectoryConcat(const VirtualDir& dir, std::string_view path) const;
+    InstallResult RawInstallNCA(std::shared_ptr<NCA> nca, const VfsCopyFunction& copy,
+                                bool overwrite_if_exists,
+                                boost::optional<NcaID> override_id = boost::none);
+    bool RawInstallYuzuMeta(const CNMT& cnmt);
+
+    VirtualDir dir;
+    RegisteredCacheParsingFunction parser;
+    // maps tid -> NcaID of meta
+    boost::container::flat_map<u64, NcaID> meta_id;
+    // maps tid -> meta
+    boost::container::flat_map<u64, CNMT> meta;
+    // maps tid -> meta for CNMT in yuzu_meta
+    boost::container::flat_map<u64, CNMT> yuzu_meta;
+};
+
+} // namespace FileSys

+ 4 - 4
src/core/file_sys/romfs.cpp

@@ -65,7 +65,7 @@ void ProcessFile(VirtualFile file, size_t file_offset, size_t data_offset, u32 t
         auto entry = GetEntry<FileEntry>(file, file_offset + this_file_offset);
 
         parent->AddFile(std::make_shared<OffsetVfsFile>(
-            file, entry.first.size, entry.first.offset + data_offset, entry.second, parent));
+            file, entry.first.size, entry.first.offset + data_offset, entry.second));
 
         if (entry.first.sibling == ROMFS_ENTRY_EMPTY)
             break;
@@ -79,7 +79,7 @@ void ProcessDirectory(VirtualFile file, size_t dir_offset, size_t file_offset, s
     while (true) {
         auto entry = GetEntry<DirectoryEntry>(file, dir_offset + this_dir_offset);
         auto current = std::make_shared<VectorVfsDirectory>(
-            std::vector<VirtualFile>{}, std::vector<VirtualDir>{}, parent, entry.second);
+            std::vector<VirtualFile>{}, std::vector<VirtualDir>{}, entry.second);
 
         if (entry.first.child_file != ROMFS_ENTRY_EMPTY) {
             ProcessFile(file, file_offset, data_offset, entry.first.child_file, current);
@@ -108,9 +108,9 @@ VirtualDir ExtractRomFS(VirtualFile file) {
     const u64 file_offset = header.file_meta.offset;
     const u64 dir_offset = header.directory_meta.offset + 4;
 
-    const auto root =
+    auto root =
         std::make_shared<VectorVfsDirectory>(std::vector<VirtualFile>{}, std::vector<VirtualDir>{},
-                                             file->GetContainingDirectory(), file->GetName());
+                                             file->GetName(), file->GetContainingDirectory());
 
     ProcessDirectory(file, dir_offset, file_offset, header.data_offset, 0, root);
 

+ 94 - 0
src/core/file_sys/vfs_concat.cpp

@@ -0,0 +1,94 @@
+// Copyright 2018 yuzu emulator team
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <algorithm>
+#include <utility>
+
+#include "core/file_sys/vfs_concat.h"
+
+namespace FileSys {
+
+VirtualFile ConcatenateFiles(std::vector<VirtualFile> files, std::string name) {
+    if (files.empty())
+        return nullptr;
+    if (files.size() == 1)
+        return files[0];
+
+    return std::shared_ptr<VfsFile>(new ConcatenatedVfsFile(std::move(files), std::move(name)));
+}
+
+ConcatenatedVfsFile::ConcatenatedVfsFile(std::vector<VirtualFile> files_, std::string name)
+    : name(std::move(name)) {
+    size_t next_offset = 0;
+    for (const auto& file : files_) {
+        files[next_offset] = file;
+        next_offset += file->GetSize();
+    }
+}
+
+std::string ConcatenatedVfsFile::GetName() const {
+    if (files.empty())
+        return "";
+    if (!name.empty())
+        return name;
+    return files.begin()->second->GetName();
+}
+
+size_t ConcatenatedVfsFile::GetSize() const {
+    if (files.empty())
+        return 0;
+    return files.rbegin()->first + files.rbegin()->second->GetSize();
+}
+
+bool ConcatenatedVfsFile::Resize(size_t new_size) {
+    return false;
+}
+
+std::shared_ptr<VfsDirectory> ConcatenatedVfsFile::GetContainingDirectory() const {
+    if (files.empty())
+        return nullptr;
+    return files.begin()->second->GetContainingDirectory();
+}
+
+bool ConcatenatedVfsFile::IsWritable() const {
+    return false;
+}
+
+bool ConcatenatedVfsFile::IsReadable() const {
+    return true;
+}
+
+size_t ConcatenatedVfsFile::Read(u8* data, size_t length, size_t offset) const {
+    auto entry = files.end();
+    for (auto iter = files.begin(); iter != files.end(); ++iter) {
+        if (iter->first > offset) {
+            entry = --iter;
+            break;
+        }
+    }
+
+    // Check if the entry should be the last one. The loop above will make it end().
+    if (entry == files.end() && offset < files.rbegin()->first + files.rbegin()->second->GetSize())
+        --entry;
+
+    if (entry == files.end())
+        return 0;
+
+    const auto remaining = entry->second->GetSize() + offset - entry->first;
+    if (length > remaining) {
+        return entry->second->Read(data, remaining, offset - entry->first) +
+               Read(data + remaining, length - remaining, offset + remaining);
+    }
+
+    return entry->second->Read(data, length, offset - entry->first);
+}
+
+size_t ConcatenatedVfsFile::Write(const u8* data, size_t length, size_t offset) {
+    return 0;
+}
+
+bool ConcatenatedVfsFile::Rename(std::string_view name) {
+    return false;
+}
+} // namespace FileSys

+ 41 - 0
src/core/file_sys/vfs_concat.h

@@ -0,0 +1,41 @@
+// Copyright 2018 yuzu emulator team
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <memory>
+#include <string_view>
+#include <boost/container/flat_map.hpp>
+#include "core/file_sys/vfs.h"
+
+namespace FileSys {
+
+// Wrapper function to allow for more efficient handling of files.size() == 0, 1 cases.
+VirtualFile ConcatenateFiles(std::vector<VirtualFile> files, std::string name = "");
+
+// Class that wraps multiple vfs files and concatenates them, making reads seamless. Currently
+// read-only.
+class ConcatenatedVfsFile : public VfsFile {
+    friend VirtualFile ConcatenateFiles(std::vector<VirtualFile> files, std::string name);
+
+    ConcatenatedVfsFile(std::vector<VirtualFile> files, std::string name);
+
+public:
+    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 Read(u8* data, size_t length, size_t offset) const override;
+    size_t Write(const u8* data, size_t length, size_t offset) override;
+    bool Rename(std::string_view name) override;
+
+private:
+    // Maps starting offset to file -- more efficient.
+    boost::container::flat_map<u64, VirtualFile> files;
+    std::string name;
+};
+
+} // namespace FileSys

+ 14 - 6
src/core/file_sys/vfs_real.cpp

@@ -83,8 +83,12 @@ VirtualFile RealVfsFilesystem::OpenFile(std::string_view path_, Mode perms) {
 
 VirtualFile RealVfsFilesystem::CreateFile(std::string_view path_, Mode perms) {
     const auto path = FileUtil::SanitizePath(path_, FileUtil::DirectorySeparator::PlatformDefault);
-    if (!FileUtil::Exists(path) && !FileUtil::CreateEmptyFile(path))
-        return nullptr;
+    const auto path_fwd = FileUtil::SanitizePath(path, FileUtil::DirectorySeparator::ForwardSlash);
+    if (!FileUtil::Exists(path)) {
+        FileUtil::CreateFullPath(path_fwd);
+        if (!FileUtil::CreateEmptyFile(path))
+            return nullptr;
+    }
     return OpenFile(path, perms);
 }
 
@@ -140,8 +144,12 @@ VirtualDir RealVfsFilesystem::OpenDirectory(std::string_view path_, Mode perms)
 
 VirtualDir RealVfsFilesystem::CreateDirectory(std::string_view path_, Mode perms) {
     const auto path = FileUtil::SanitizePath(path_, FileUtil::DirectorySeparator::PlatformDefault);
-    if (!FileUtil::Exists(path) && !FileUtil::CreateDir(path))
-        return nullptr;
+    const auto path_fwd = FileUtil::SanitizePath(path, FileUtil::DirectorySeparator::ForwardSlash);
+    if (!FileUtil::Exists(path)) {
+        FileUtil::CreateFullPath(path_fwd);
+        if (!FileUtil::CreateDir(path))
+            return nullptr;
+    }
     // Cannot use make_shared as RealVfsDirectory constructor is private
     return std::shared_ptr<RealVfsDirectory>(new RealVfsDirectory(*this, path, perms));
 }
@@ -306,14 +314,14 @@ RealVfsDirectory::RealVfsDirectory(RealVfsFilesystem& base_, const std::string&
 
 std::shared_ptr<VfsFile> RealVfsDirectory::GetFileRelative(std::string_view path) const {
     const auto full_path = FileUtil::SanitizePath(this->path + DIR_SEP + std::string(path));
-    if (!FileUtil::Exists(full_path))
+    if (!FileUtil::Exists(full_path) || FileUtil::IsDirectory(full_path))
         return nullptr;
     return base.OpenFile(full_path, perms);
 }
 
 std::shared_ptr<VfsDirectory> RealVfsDirectory::GetDirectoryRelative(std::string_view path) const {
     const auto full_path = FileUtil::SanitizePath(this->path + DIR_SEP + std::string(path));
-    if (!FileUtil::Exists(full_path))
+    if (!FileUtil::Exists(full_path) || !FileUtil::IsDirectory(full_path))
         return nullptr;
     return base.OpenDirectory(full_path, perms);
 }

+ 0 - 1
src/core/file_sys/vfs_real.h

@@ -5,7 +5,6 @@
 #pragma once
 
 #include <string_view>
-
 #include <boost/container/flat_map.hpp>
 #include "common/file_util.h"
 #include "core/file_sys/mode.h"

+ 2 - 2
src/core/file_sys/vfs_vector.cpp

@@ -8,8 +8,8 @@
 
 namespace FileSys {
 VectorVfsDirectory::VectorVfsDirectory(std::vector<VirtualFile> files_,
-                                       std::vector<VirtualDir> dirs_, VirtualDir parent_,
-                                       std::string name_)
+                                       std::vector<VirtualDir> dirs_, std::string name_,
+                                       VirtualDir parent_)
     : files(std::move(files_)), dirs(std::move(dirs_)), parent(std::move(parent_)),
       name(std::move(name_)) {}
 

+ 2 - 2
src/core/file_sys/vfs_vector.h

@@ -13,8 +13,8 @@ namespace FileSys {
 class VectorVfsDirectory : public VfsDirectory {
 public:
     explicit VectorVfsDirectory(std::vector<VirtualFile> files = {},
-                                std::vector<VirtualDir> dirs = {}, VirtualDir parent = nullptr,
-                                std::string name = "");
+                                std::vector<VirtualDir> dirs = {}, std::string name = "",
+                                VirtualDir parent = nullptr);
 
     std::vector<std::shared_ptr<VfsFile>> GetFiles() const override;
     std::vector<std::shared_ptr<VfsDirectory>> GetSubdirectories() const override;

+ 19 - 0
src/core/hle/service/filesystem/filesystem.cpp

@@ -226,6 +226,7 @@ ResultVal<FileSys::EntryType> VfsDirectoryServiceWrapper::GetEntryType(
 static std::unique_ptr<FileSys::RomFSFactory> romfs_factory;
 static std::unique_ptr<FileSys::SaveDataFactory> save_data_factory;
 static std::unique_ptr<FileSys::SDMCFactory> sdmc_factory;
+static std::unique_ptr<FileSys::BISFactory> bis_factory;
 
 ResultCode RegisterRomFS(std::unique_ptr<FileSys::RomFSFactory>&& factory) {
     ASSERT_MSG(romfs_factory == nullptr, "Tried to register a second RomFS");
@@ -248,6 +249,13 @@ ResultCode RegisterSDMC(std::unique_ptr<FileSys::SDMCFactory>&& factory) {
     return RESULT_SUCCESS;
 }
 
+ResultCode RegisterBIS(std::unique_ptr<FileSys::BISFactory>&& factory) {
+    ASSERT_MSG(bis_factory == nullptr, "Tried to register a second BIS");
+    bis_factory = std::move(factory);
+    LOG_DEBUG(Service_FS, "Registred BIS");
+    return RESULT_SUCCESS;
+}
+
 ResultVal<FileSys::VirtualFile> OpenRomFS(u64 title_id) {
     LOG_TRACE(Service_FS, "Opening RomFS for title_id={:016X}", title_id);
 
@@ -281,6 +289,14 @@ ResultVal<FileSys::VirtualDir> OpenSDMC() {
     return sdmc_factory->Open();
 }
 
+std::shared_ptr<FileSys::RegisteredCache> GetSystemNANDContents() {
+    return bis_factory->GetSystemNANDContents();
+}
+
+std::shared_ptr<FileSys::RegisteredCache> GetUserNANDContents() {
+    return bis_factory->GetUserNANDContents();
+}
+
 void RegisterFileSystems(const FileSys::VirtualFilesystem& vfs) {
     romfs_factory = nullptr;
     save_data_factory = nullptr;
@@ -291,6 +307,9 @@ void RegisterFileSystems(const FileSys::VirtualFilesystem& vfs) {
     auto sd_directory = vfs->OpenDirectory(FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir),
                                            FileSys::Mode::ReadWrite);
 
+    if (bis_factory == nullptr)
+        bis_factory = std::make_unique<FileSys::BISFactory>(nand_directory);
+
     auto savedata = std::make_unique<FileSys::SaveDataFactory>(std::move(nand_directory));
     save_data_factory = std::move(savedata);
 

+ 4 - 4
src/core/hle/service/filesystem/filesystem.h

@@ -6,6 +6,7 @@
 
 #include <memory>
 #include "common/common_types.h"
+#include "core/file_sys/bis_factory.h"
 #include "core/file_sys/directory.h"
 #include "core/file_sys/mode.h"
 #include "core/file_sys/romfs_factory.h"
@@ -24,16 +25,15 @@ namespace FileSystem {
 ResultCode RegisterRomFS(std::unique_ptr<FileSys::RomFSFactory>&& factory);
 ResultCode RegisterSaveData(std::unique_ptr<FileSys::SaveDataFactory>&& factory);
 ResultCode RegisterSDMC(std::unique_ptr<FileSys::SDMCFactory>&& factory);
+ResultCode RegisterBIS(std::unique_ptr<FileSys::BISFactory>&& factory);
 
-// TODO(DarkLordZach): BIS Filesystem
-// ResultCode RegisterBIS(std::unique_ptr<FileSys::BISFactory>&& factory);
 ResultVal<FileSys::VirtualFile> OpenRomFS(u64 title_id);
 ResultVal<FileSys::VirtualDir> OpenSaveData(FileSys::SaveDataSpaceId space,
                                             FileSys::SaveDataDescriptor save_struct);
 ResultVal<FileSys::VirtualDir> OpenSDMC();
 
-// TODO(DarkLordZach): BIS Filesystem
-// ResultVal<std::unique_ptr<FileSys::FileSystemBackend>> OpenBIS();
+std::shared_ptr<FileSys::RegisteredCache> GetSystemNANDContents();
+std::shared_ptr<FileSys::RegisteredCache> GetUserNANDContents();
 
 /// Registers all Filesystem services with the specified service manager.
 void InstallInterfaces(SM::ServiceManager& service_manager, const FileSys::VirtualFilesystem& vfs);

+ 2 - 0
src/core/loader/loader.cpp

@@ -41,6 +41,8 @@ FileType IdentifyFile(FileSys::VirtualFile file) {
 FileType GuessFromFilename(const std::string& name) {
     if (name == "main")
         return FileType::DeconstructedRomDirectory;
+    if (name == "00")
+        return FileType::NCA;
 
     const std::string extension =
         Common::ToLower(std::string(FileUtil::GetExtensionFromFilename(name)));

+ 74 - 22
src/yuzu/game_list.cpp

@@ -2,6 +2,7 @@
 // Licensed under GPLv2 or any later version
 // Refer to the license.txt file included.
 
+#include <regex>
 #include <QApplication>
 #include <QDir>
 #include <QFileInfo>
@@ -402,12 +403,72 @@ void GameList::RefreshGameDirectory() {
     }
 }
 
-void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion) {
-    boost::container::flat_map<u64, std::shared_ptr<FileSys::NCA>> nca_control_map;
+static void GetMetadataFromControlNCA(const std::shared_ptr<FileSys::NCA>& nca,
+                                      std::vector<u8>& icon, std::string& name) {
+    const auto control_dir = FileSys::ExtractRomFS(nca->GetRomFS());
+    if (control_dir == nullptr)
+        return;
+
+    const auto nacp_file = control_dir->GetFile("control.nacp");
+    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;
+        }
+    }
+}
+
+void GameListWorker::AddInstalledTitlesToGameList() {
+    const auto usernand = Service::FileSystem::GetUserNANDContents();
+    const auto installed_games = usernand->ListEntriesFilter(FileSys::TitleType::Application,
+                                                             FileSys::ContentRecordType::Program);
+
+    for (const auto& game : installed_games) {
+        const auto& file = usernand->GetEntryRaw(game);
+        std::unique_ptr<Loader::AppLoader> loader = Loader::GetLoader(file);
+        if (!loader)
+            continue;
+
+        std::vector<u8> icon;
+        std::string name;
+        u64 program_id;
+        loader->ReadProgramId(program_id);
+
+        const auto& control =
+            usernand->GetEntry(game.title_id, FileSys::ContentRecordType::Control);
+        if (control != nullptr)
+            GetMetadataFromControlNCA(control, icon, name);
+        emit EntryReady({
+            new GameListItemPath(
+                FormatGameName(file->GetFullPath()), icon, QString::fromStdString(name),
+                QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType())),
+                program_id),
+            new GameListItem(
+                QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))),
+            new GameListItemSize(file->GetSize()),
+        });
+    }
+
+    const auto control_data = usernand->ListEntriesFilter(FileSys::TitleType::Application,
+                                                          FileSys::ContentRecordType::Control);
+
+    for (const auto& entry : control_data) {
+        const auto nca = usernand->GetEntry(entry);
+        if (nca != nullptr)
+            nca_control_map.insert_or_assign(entry.title_id, nca);
+    }
+}
 
-    const auto nca_control_callback =
-        [this, &nca_control_map](u64* num_entries_out, const std::string& directory,
-                                 const std::string& virtual_name) -> bool {
+void GameListWorker::FillControlMap(const std::string& dir_path) {
+    const auto nca_control_callback = [this](u64* num_entries_out, const std::string& directory,
+                                             const std::string& virtual_name) -> bool {
         std::string physical_name = directory + DIR_SEP + virtual_name;
 
         if (stop_processing)
@@ -425,10 +486,11 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign
     };
 
     FileUtil::ForeachDirectoryEntry(nullptr, dir_path, nca_control_callback);
+}
 
-    const auto callback = [this, recursion,
-                           &nca_control_map](u64* num_entries_out, const std::string& directory,
-                                             const std::string& virtual_name) -> bool {
+void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion) {
+    const auto callback = [this, recursion](u64* num_entries_out, const std::string& directory,
+                                            const std::string& virtual_name) -> bool {
         std::string physical_name = directory + DIR_SEP + virtual_name;
 
         if (stop_processing)
@@ -458,20 +520,7 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign
                 // Use from metadata pool.
                 if (nca_control_map.find(program_id) != nca_control_map.end()) {
                     const auto nca = nca_control_map[program_id];
-                    const auto control_dir = FileSys::ExtractRomFS(nca->GetRomFS());
-
-                    const auto nacp_file = control_dir->GetFile("control.nacp");
-                    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;
-                        }
-                    }
+                    GetMetadataFromControlNCA(nca, icon, name);
                 }
             }
 
@@ -498,7 +547,10 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign
 void GameListWorker::run() {
     stop_processing = false;
     watch_list.append(dir_path);
+    FillControlMap(dir_path.toStdString());
+    AddInstalledTitlesToGameList();
     AddFstEntriesToGameList(dir_path.toStdString(), deep_scan ? 256 : 0);
+    nca_control_map.clear();
     emit Finished(watch_list);
 }
 

+ 3 - 0
src/yuzu/game_list_p.h

@@ -163,10 +163,13 @@ signals:
 
 private:
     FileSys::VirtualFilesystem vfs;
+    std::map<u64, std::shared_ptr<FileSys::NCA>> nca_control_map;
     QStringList watch_list;
     QString dir_path;
     bool deep_scan;
     std::atomic_bool stop_processing;
 
+    void AddInstalledTitlesToGameList();
+    void FillControlMap(const std::string& dir_path);
     void AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion = 0);
 };

+ 143 - 0
src/yuzu/main.cpp

@@ -27,6 +27,7 @@
 #include "common/string_util.h"
 #include "core/core.h"
 #include "core/crypto/key_manager.h"
+#include "core/file_sys/card_image.h"
 #include "core/file_sys/vfs_real.h"
 #include "core/gdbstub/gdbstub.h"
 #include "core/loader/loader.h"
@@ -117,6 +118,9 @@ GMainWindow::GMainWindow()
                        .arg(Common::g_build_name, Common::g_scm_branch, Common::g_scm_desc));
     show();
 
+    // Necessary to load titles from nand in gamelist.
+    Service::FileSystem::RegisterBIS(std::make_unique<FileSys::BISFactory>(vfs->OpenDirectory(
+        FileUtil::GetUserPath(FileUtil::UserPath::NANDDir), FileSys::Mode::ReadWrite)));
     game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan);
 
     // Show one-time "callout" messages to the user
@@ -312,6 +316,8 @@ void GMainWindow::ConnectMenuEvents() {
     // File
     connect(ui.action_Load_File, &QAction::triggered, this, &GMainWindow::OnMenuLoadFile);
     connect(ui.action_Load_Folder, &QAction::triggered, this, &GMainWindow::OnMenuLoadFolder);
+    connect(ui.action_Install_File_NAND, &QAction::triggered, this,
+            &GMainWindow::OnMenuInstallToNAND);
     connect(ui.action_Select_Game_List_Root, &QAction::triggered, this,
             &GMainWindow::OnMenuSelectGameListRoot);
     connect(ui.action_Exit, &QAction::triggered, this, &QMainWindow::close);
@@ -615,6 +621,143 @@ void GMainWindow::OnMenuLoadFolder() {
     }
 }
 
+void GMainWindow::OnMenuInstallToNAND() {
+    const QString file_filter =
+        tr("Installable Switch File (*.nca *.xci);;Nintendo Content Archive (*.nca);;NX Cartridge "
+           "Image (*.xci)");
+    QString filename = QFileDialog::getOpenFileName(this, tr("Install File"),
+                                                    UISettings::values.roms_path, file_filter);
+
+    const auto qt_raw_copy = [this](FileSys::VirtualFile src, FileSys::VirtualFile dest) {
+        if (src == nullptr || dest == nullptr)
+            return false;
+        if (!dest->Resize(src->GetSize()))
+            return false;
+
+        QProgressDialog progress(fmt::format("Installing file \"{}\"...", src->GetName()).c_str(),
+                                 "Cancel", 0, src->GetSize() / 0x1000, this);
+        progress.setWindowModality(Qt::WindowModal);
+
+        std::array<u8, 0x1000> buffer{};
+        for (size_t i = 0; i < src->GetSize(); i += 0x1000) {
+            if (progress.wasCanceled()) {
+                dest->Resize(0);
+                return false;
+            }
+
+            progress.setValue(i / 0x1000);
+            const auto read = src->Read(buffer.data(), buffer.size(), i);
+            dest->Write(buffer.data(), read, i);
+        }
+
+        return true;
+    };
+
+    const auto success = [this]() {
+        QMessageBox::information(this, tr("Successfully Installed"),
+                                 tr("The file was successfully installed."));
+        game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan);
+    };
+
+    const auto failed = [this]() {
+        QMessageBox::warning(
+            this, tr("Failed to Install"),
+            tr("There was an error while attempting to install the provided file. It "
+               "could have an incorrect format or be missing metadata. Please "
+               "double-check your file and try again."));
+    };
+
+    const auto overwrite = [this]() {
+        return QMessageBox::question(this, "Failed to Install",
+                                     "The file you are attempting to install already exists "
+                                     "in the cache. Would you like to overwrite it?") ==
+               QMessageBox::Yes;
+    };
+
+    if (!filename.isEmpty()) {
+        if (filename.endsWith("xci", Qt::CaseInsensitive)) {
+            const auto xci = std::make_shared<FileSys::XCI>(
+                vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
+            if (xci->GetStatus() != Loader::ResultStatus::Success) {
+                failed();
+                return;
+            }
+            const auto res =
+                Service::FileSystem::GetUserNANDContents()->InstallEntry(xci, false, qt_raw_copy);
+            if (res == FileSys::InstallResult::Success) {
+                success();
+            } else {
+                if (res == FileSys::InstallResult::ErrorAlreadyExists) {
+                    if (overwrite()) {
+                        const auto res2 = Service::FileSystem::GetUserNANDContents()->InstallEntry(
+                            xci, true, qt_raw_copy);
+                        if (res2 == FileSys::InstallResult::Success) {
+                            success();
+                        } else {
+                            failed();
+                        }
+                    }
+                } else {
+                    failed();
+                }
+            }
+        } else {
+            const auto nca = std::make_shared<FileSys::NCA>(
+                vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
+            if (nca->GetStatus() != Loader::ResultStatus::Success) {
+                failed();
+                return;
+            }
+
+            static const QStringList tt_options{"System Application",
+                                                "System Archive",
+                                                "System Application Update",
+                                                "Firmware Package (Type A)",
+                                                "Firmware Package (Type B)",
+                                                "Game",
+                                                "Game Update",
+                                                "Game DLC",
+                                                "Delta Title"};
+            bool ok;
+            const auto item = QInputDialog::getItem(
+                this, tr("Select NCA Install Type..."),
+                tr("Please select the type of title you would like to install this NCA as:\n(In "
+                   "most instances, the default 'Game' is fine.)"),
+                tt_options, 5, false, &ok);
+
+            auto index = tt_options.indexOf(item);
+            if (!ok || index == -1) {
+                QMessageBox::warning(this, tr("Failed to Install"),
+                                     tr("The title type you selected for the NCA is invalid."));
+                return;
+            }
+
+            if (index >= 5)
+                index += 0x7B;
+
+            const auto res = Service::FileSystem::GetUserNANDContents()->InstallEntry(
+                nca, static_cast<FileSys::TitleType>(index), false, qt_raw_copy);
+            if (res == FileSys::InstallResult::Success) {
+                success();
+            } else {
+                if (res == FileSys::InstallResult::ErrorAlreadyExists) {
+                    if (overwrite()) {
+                        const auto res2 = Service::FileSystem::GetUserNANDContents()->InstallEntry(
+                            nca, static_cast<FileSys::TitleType>(index), true, qt_raw_copy);
+                        if (res2 == FileSys::InstallResult::Success) {
+                            success();
+                        } else {
+                            failed();
+                        }
+                    }
+                } else {
+                    failed();
+                }
+            }
+        }
+    }
+}
+
 void GMainWindow::OnMenuSelectGameListRoot() {
     QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory"));
     if (!dir_path.isEmpty()) {

+ 1 - 0
src/yuzu/main.h

@@ -125,6 +125,7 @@ private slots:
     void OnGameListOpenSaveFolder(u64 program_id);
     void OnMenuLoadFile();
     void OnMenuLoadFolder();
+    void OnMenuInstallToNAND();
     /// Called whenever a user selects the "File->Select Game List Root" menu item
     void OnMenuSelectGameListRoot();
     void OnMenuRecentFile();

+ 7 - 0
src/yuzu/main.ui

@@ -57,6 +57,8 @@
       <string>Recent Files</string>
      </property>
     </widget>
+     <addaction name="action_Install_File_NAND" />
+     <addaction name="separator"/>
     <addaction name="action_Load_File"/>
     <addaction name="action_Load_Folder"/>
     <addaction name="separator"/>
@@ -102,6 +104,11 @@
    <addaction name="menu_View"/>
    <addaction name="menu_Help"/>
   </widget>
+   <action name="action_Install_File_NAND">
+     <property name="text">
+       <string>Install File to NAND...</string>
+     </property>
+   </action>
   <action name="action_Load_File">
    <property name="text">
     <string>Load File...</string>