diff options
Diffstat (limited to 'src')
34 files changed, 1437 insertions, 80 deletions
diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 939b8a7d3..d9424ea91 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt | |||
| @@ -38,6 +38,8 @@ add_library(common STATIC | |||
| 38 | file_util.cpp | 38 | file_util.cpp |
| 39 | file_util.h | 39 | file_util.h |
| 40 | hash.h | 40 | hash.h |
| 41 | hex_util.cpp | ||
| 42 | hex_util.h | ||
| 41 | logging/backend.cpp | 43 | logging/backend.cpp |
| 42 | logging/backend.h | 44 | logging/backend.h |
| 43 | logging/filter.cpp | 45 | logging/filter.cpp |
diff --git a/src/common/file_util.cpp b/src/common/file_util.cpp index 3ce590062..b30a67ff9 100644 --- a/src/common/file_util.cpp +++ b/src/common/file_util.cpp | |||
| @@ -750,6 +750,12 @@ std::string GetHactoolConfigurationPath() { | |||
| 750 | #endif | 750 | #endif |
| 751 | } | 751 | } |
| 752 | 752 | ||
| 753 | std::string GetNANDRegistrationDir(bool system) { | ||
| 754 | if (system) | ||
| 755 | return GetUserPath(UserPath::NANDDir) + "system/Contents/registered/"; | ||
| 756 | return GetUserPath(UserPath::NANDDir) + "user/Contents/registered/"; | ||
| 757 | } | ||
| 758 | |||
| 753 | size_t WriteStringToFile(bool text_file, const std::string& str, const char* filename) { | 759 | size_t WriteStringToFile(bool text_file, const std::string& str, const char* filename) { |
| 754 | return FileUtil::IOFile(filename, text_file ? "w" : "wb").WriteBytes(str.data(), str.size()); | 760 | return FileUtil::IOFile(filename, text_file ? "w" : "wb").WriteBytes(str.data(), str.size()); |
| 755 | } | 761 | } |
diff --git a/src/common/file_util.h b/src/common/file_util.h index 2711872ae..2f13d0b6b 100644 --- a/src/common/file_util.h +++ b/src/common/file_util.h | |||
| @@ -129,6 +129,8 @@ const std::string& GetUserPath(UserPath path, const std::string& new_path = ""); | |||
| 129 | 129 | ||
| 130 | std::string GetHactoolConfigurationPath(); | 130 | std::string GetHactoolConfigurationPath(); |
| 131 | 131 | ||
| 132 | std::string GetNANDRegistrationDir(bool system = false); | ||
| 133 | |||
| 132 | // Returns the path to where the sys file are | 134 | // Returns the path to where the sys file are |
| 133 | std::string GetSysDirectory(); | 135 | std::string GetSysDirectory(); |
| 134 | 136 | ||
diff --git a/src/common/hex_util.cpp b/src/common/hex_util.cpp new file mode 100644 index 000000000..ae17c89d4 --- /dev/null +++ b/src/common/hex_util.cpp | |||
| @@ -0,0 +1,27 @@ | |||
| 1 | // Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project | ||
| 2 | // Licensed under GPLv2 or any later version | ||
| 3 | // Refer to the license.txt file included. | ||
| 4 | |||
| 5 | #include "common/hex_util.h" | ||
| 6 | |||
| 7 | u8 ToHexNibble(char c1) { | ||
| 8 | if (c1 >= 65 && c1 <= 70) | ||
| 9 | return c1 - 55; | ||
| 10 | if (c1 >= 97 && c1 <= 102) | ||
| 11 | return c1 - 87; | ||
| 12 | if (c1 >= 48 && c1 <= 57) | ||
| 13 | return c1 - 48; | ||
| 14 | throw std::logic_error("Invalid hex digit"); | ||
| 15 | } | ||
| 16 | |||
| 17 | std::array<u8, 16> operator""_array16(const char* str, size_t len) { | ||
| 18 | if (len != 32) | ||
| 19 | throw std::logic_error("Not of correct size."); | ||
| 20 | return HexStringToArray<16>(str); | ||
| 21 | } | ||
| 22 | |||
| 23 | std::array<u8, 32> operator""_array32(const char* str, size_t len) { | ||
| 24 | if (len != 64) | ||
| 25 | throw std::logic_error("Not of correct size."); | ||
| 26 | return HexStringToArray<32>(str); | ||
| 27 | } | ||
diff --git a/src/common/hex_util.h b/src/common/hex_util.h new file mode 100644 index 000000000..13d586015 --- /dev/null +++ b/src/common/hex_util.h | |||
| @@ -0,0 +1,37 @@ | |||
| 1 | // Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project | ||
| 2 | // Licensed under GPLv2 or any later version | ||
| 3 | // Refer to the license.txt file included. | ||
| 4 | |||
| 5 | #pragma once | ||
| 6 | |||
| 7 | #include <array> | ||
| 8 | #include <cstddef> | ||
| 9 | #include <string> | ||
| 10 | #include <fmt/format.h> | ||
| 11 | #include "common/common_types.h" | ||
| 12 | |||
| 13 | u8 ToHexNibble(char c1); | ||
| 14 | |||
| 15 | template <size_t Size, bool le = false> | ||
| 16 | std::array<u8, Size> HexStringToArray(std::string_view str) { | ||
| 17 | std::array<u8, Size> out{}; | ||
| 18 | if constexpr (le) { | ||
| 19 | for (size_t i = 2 * Size - 2; i <= 2 * Size; i -= 2) | ||
| 20 | out[i / 2] = (ToHexNibble(str[i]) << 4) | ToHexNibble(str[i + 1]); | ||
| 21 | } else { | ||
| 22 | for (size_t i = 0; i < 2 * Size; i += 2) | ||
| 23 | out[i / 2] = (ToHexNibble(str[i]) << 4) | ToHexNibble(str[i + 1]); | ||
| 24 | } | ||
| 25 | return out; | ||
| 26 | } | ||
| 27 | |||
| 28 | template <size_t Size> | ||
| 29 | std::string HexArrayToString(std::array<u8, Size> array, bool upper = true) { | ||
| 30 | std::string out; | ||
| 31 | for (u8 c : array) | ||
| 32 | out += fmt::format(upper ? "{:02X}" : "{:02x}", c); | ||
| 33 | return out; | ||
| 34 | } | ||
| 35 | |||
| 36 | std::array<u8, 0x10> operator"" _array16(const char* str, size_t len); | ||
| 37 | std::array<u8, 0x20> operator"" _array32(const char* str, size_t len); | ||
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 0b0ae5ccc..67ad6109a 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt | |||
| @@ -20,6 +20,8 @@ add_library(core STATIC | |||
| 20 | crypto/key_manager.h | 20 | crypto/key_manager.h |
| 21 | crypto/ctr_encryption_layer.cpp | 21 | crypto/ctr_encryption_layer.cpp |
| 22 | crypto/ctr_encryption_layer.h | 22 | crypto/ctr_encryption_layer.h |
| 23 | file_sys/bis_factory.cpp | ||
| 24 | file_sys/bis_factory.h | ||
| 23 | file_sys/card_image.cpp | 25 | file_sys/card_image.cpp |
| 24 | file_sys/card_image.h | 26 | file_sys/card_image.h |
| 25 | file_sys/content_archive.cpp | 27 | file_sys/content_archive.cpp |
| @@ -29,10 +31,14 @@ add_library(core STATIC | |||
| 29 | file_sys/directory.h | 31 | file_sys/directory.h |
| 30 | file_sys/errors.h | 32 | file_sys/errors.h |
| 31 | file_sys/mode.h | 33 | file_sys/mode.h |
| 34 | file_sys/nca_metadata.cpp | ||
| 35 | file_sys/nca_metadata.h | ||
| 32 | file_sys/partition_filesystem.cpp | 36 | file_sys/partition_filesystem.cpp |
| 33 | file_sys/partition_filesystem.h | 37 | file_sys/partition_filesystem.h |
| 34 | file_sys/program_metadata.cpp | 38 | file_sys/program_metadata.cpp |
| 35 | file_sys/program_metadata.h | 39 | file_sys/program_metadata.h |
| 40 | file_sys/registered_cache.cpp | ||
| 41 | file_sys/registered_cache.h | ||
| 36 | file_sys/romfs.cpp | 42 | file_sys/romfs.cpp |
| 37 | file_sys/romfs.h | 43 | file_sys/romfs.h |
| 38 | file_sys/romfs_factory.cpp | 44 | file_sys/romfs_factory.cpp |
| @@ -43,6 +49,8 @@ add_library(core STATIC | |||
| 43 | file_sys/sdmc_factory.h | 49 | file_sys/sdmc_factory.h |
| 44 | file_sys/vfs.cpp | 50 | file_sys/vfs.cpp |
| 45 | file_sys/vfs.h | 51 | file_sys/vfs.h |
| 52 | file_sys/vfs_concat.cpp | ||
| 53 | file_sys/vfs_concat.h | ||
| 46 | file_sys/vfs_offset.cpp | 54 | file_sys/vfs_offset.cpp |
| 47 | file_sys/vfs_offset.h | 55 | file_sys/vfs_offset.h |
| 48 | file_sys/vfs_real.cpp | 56 | file_sys/vfs_real.cpp |
diff --git a/src/core/core.cpp b/src/core/core.cpp index 83d4d742b..28038ff6f 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp | |||
| @@ -5,6 +5,7 @@ | |||
| 5 | #include <memory> | 5 | #include <memory> |
| 6 | #include <utility> | 6 | #include <utility> |
| 7 | #include "common/logging/log.h" | 7 | #include "common/logging/log.h" |
| 8 | #include "common/string_util.h" | ||
| 8 | #include "core/core.h" | 9 | #include "core/core.h" |
| 9 | #include "core/core_timing.h" | 10 | #include "core/core_timing.h" |
| 10 | #include "core/gdbstub/gdbstub.h" | 11 | #include "core/gdbstub/gdbstub.h" |
| @@ -17,6 +18,7 @@ | |||
| 17 | #include "core/hle/service/sm/sm.h" | 18 | #include "core/hle/service/sm/sm.h" |
| 18 | #include "core/loader/loader.h" | 19 | #include "core/loader/loader.h" |
| 19 | #include "core/settings.h" | 20 | #include "core/settings.h" |
| 21 | #include "file_sys/vfs_concat.h" | ||
| 20 | #include "file_sys/vfs_real.h" | 22 | #include "file_sys/vfs_real.h" |
| 21 | #include "video_core/renderer_base.h" | 23 | #include "video_core/renderer_base.h" |
| 22 | #include "video_core/video_core.h" | 24 | #include "video_core/video_core.h" |
| @@ -88,8 +90,39 @@ System::ResultStatus System::SingleStep() { | |||
| 88 | return RunLoop(false); | 90 | return RunLoop(false); |
| 89 | } | 91 | } |
| 90 | 92 | ||
| 93 | static FileSys::VirtualFile GetGameFileFromPath(const FileSys::VirtualFilesystem& vfs, | ||
| 94 | const std::string& path) { | ||
| 95 | // To account for split 00+01+etc files. | ||
| 96 | std::string dir_name; | ||
| 97 | std::string filename; | ||
| 98 | Common::SplitPath(path, &dir_name, &filename, nullptr); | ||
| 99 | if (filename == "00") { | ||
| 100 | const auto dir = vfs->OpenDirectory(dir_name, FileSys::Mode::Read); | ||
| 101 | std::vector<FileSys::VirtualFile> concat; | ||
| 102 | for (u8 i = 0; i < 0x10; ++i) { | ||
| 103 | auto next = dir->GetFile(fmt::format("{:02X}", i)); | ||
| 104 | if (next != nullptr) | ||
| 105 | concat.push_back(std::move(next)); | ||
| 106 | else { | ||
| 107 | next = dir->GetFile(fmt::format("{:02x}", i)); | ||
| 108 | if (next != nullptr) | ||
| 109 | concat.push_back(std::move(next)); | ||
| 110 | else | ||
| 111 | break; | ||
| 112 | } | ||
| 113 | } | ||
| 114 | |||
| 115 | if (concat.empty()) | ||
| 116 | return nullptr; | ||
| 117 | |||
| 118 | return FileSys::ConcatenateFiles(concat, dir->GetName()); | ||
| 119 | } | ||
| 120 | |||
| 121 | return vfs->OpenFile(path, FileSys::Mode::Read); | ||
| 122 | } | ||
| 123 | |||
| 91 | System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::string& filepath) { | 124 | System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::string& filepath) { |
| 92 | app_loader = Loader::GetLoader(virtual_filesystem->OpenFile(filepath, FileSys::Mode::Read)); | 125 | app_loader = Loader::GetLoader(GetGameFileFromPath(virtual_filesystem, filepath)); |
| 93 | 126 | ||
| 94 | if (!app_loader) { | 127 | if (!app_loader) { |
| 95 | LOG_CRITICAL(Core, "Failed to obtain loader for {}!", filepath); | 128 | LOG_CRITICAL(Core, "Failed to obtain loader for {}!", filepath); |
diff --git a/src/core/crypto/key_manager.cpp b/src/core/crypto/key_manager.cpp index fc45e7ab5..94d92579f 100644 --- a/src/core/crypto/key_manager.cpp +++ b/src/core/crypto/key_manager.cpp | |||
| @@ -10,44 +10,13 @@ | |||
| 10 | #include <string_view> | 10 | #include <string_view> |
| 11 | #include "common/common_paths.h" | 11 | #include "common/common_paths.h" |
| 12 | #include "common/file_util.h" | 12 | #include "common/file_util.h" |
| 13 | #include "common/hex_util.h" | ||
| 14 | #include "common/logging/log.h" | ||
| 13 | #include "core/crypto/key_manager.h" | 15 | #include "core/crypto/key_manager.h" |
| 14 | #include "core/settings.h" | 16 | #include "core/settings.h" |
| 15 | 17 | ||
| 16 | namespace Core::Crypto { | 18 | namespace Core::Crypto { |
| 17 | 19 | ||
| 18 | static u8 ToHexNibble(char c1) { | ||
| 19 | if (c1 >= 65 && c1 <= 70) | ||
| 20 | return c1 - 55; | ||
| 21 | if (c1 >= 97 && c1 <= 102) | ||
| 22 | return c1 - 87; | ||
| 23 | if (c1 >= 48 && c1 <= 57) | ||
| 24 | return c1 - 48; | ||
| 25 | throw std::logic_error("Invalid hex digit"); | ||
| 26 | } | ||
| 27 | |||
| 28 | template <size_t Size> | ||
| 29 | static std::array<u8, Size> HexStringToArray(std::string_view str) { | ||
| 30 | std::array<u8, Size> out{}; | ||
| 31 | for (size_t i = 0; i < 2 * Size; i += 2) { | ||
| 32 | auto d1 = str[i]; | ||
| 33 | auto d2 = str[i + 1]; | ||
| 34 | out[i / 2] = (ToHexNibble(d1) << 4) | ToHexNibble(d2); | ||
| 35 | } | ||
| 36 | return out; | ||
| 37 | } | ||
| 38 | |||
| 39 | std::array<u8, 16> operator""_array16(const char* str, size_t len) { | ||
| 40 | if (len != 32) | ||
| 41 | throw std::logic_error("Not of correct size."); | ||
| 42 | return HexStringToArray<16>(str); | ||
| 43 | } | ||
| 44 | |||
| 45 | std::array<u8, 32> operator""_array32(const char* str, size_t len) { | ||
| 46 | if (len != 64) | ||
| 47 | throw std::logic_error("Not of correct size."); | ||
| 48 | return HexStringToArray<32>(str); | ||
| 49 | } | ||
| 50 | |||
| 51 | KeyManager::KeyManager() { | 20 | KeyManager::KeyManager() { |
| 52 | // Initialize keys | 21 | // Initialize keys |
| 53 | const std::string hactool_keys_dir = FileUtil::GetHactoolConfigurationPath(); | 22 | const std::string hactool_keys_dir = FileUtil::GetHactoolConfigurationPath(); |
diff --git a/src/core/crypto/key_manager.h b/src/core/crypto/key_manager.h index c4c53cefc..0c62d4421 100644 --- a/src/core/crypto/key_manager.h +++ b/src/core/crypto/key_manager.h | |||
| @@ -87,9 +87,6 @@ struct hash<Core::Crypto::KeyIndex<KeyType>> { | |||
| 87 | 87 | ||
| 88 | namespace Core::Crypto { | 88 | namespace Core::Crypto { |
| 89 | 89 | ||
| 90 | std::array<u8, 0x10> operator"" _array16(const char* str, size_t len); | ||
| 91 | std::array<u8, 0x20> operator"" _array32(const char* str, size_t len); | ||
| 92 | |||
| 93 | class KeyManager { | 90 | class KeyManager { |
| 94 | public: | 91 | public: |
| 95 | KeyManager(); | 92 | KeyManager(); |
diff --git a/src/core/file_sys/bis_factory.cpp b/src/core/file_sys/bis_factory.cpp new file mode 100644 index 000000000..ae4e33800 --- /dev/null +++ b/src/core/file_sys/bis_factory.cpp | |||
| @@ -0,0 +1,31 @@ | |||
| 1 | // Copyright 2018 yuzu emulator team | ||
| 2 | // Licensed under GPLv2 or any later version | ||
| 3 | // Refer to the license.txt file included. | ||
| 4 | |||
| 5 | #include "core/file_sys/bis_factory.h" | ||
| 6 | |||
| 7 | namespace FileSys { | ||
| 8 | |||
| 9 | static VirtualDir GetOrCreateDirectory(const VirtualDir& dir, std::string_view path) { | ||
| 10 | const auto res = dir->GetDirectoryRelative(path); | ||
| 11 | if (res == nullptr) | ||
| 12 | return dir->CreateDirectoryRelative(path); | ||
| 13 | return res; | ||
| 14 | } | ||
| 15 | |||
| 16 | BISFactory::BISFactory(VirtualDir nand_root_) | ||
| 17 | : nand_root(std::move(nand_root_)), | ||
| 18 | sysnand_cache(std::make_shared<RegisteredCache>( | ||
| 19 | GetOrCreateDirectory(nand_root, "/system/Contents/registered"))), | ||
| 20 | usrnand_cache(std::make_shared<RegisteredCache>( | ||
| 21 | GetOrCreateDirectory(nand_root, "/user/Contents/registered"))) {} | ||
| 22 | |||
| 23 | std::shared_ptr<RegisteredCache> BISFactory::GetSystemNANDContents() const { | ||
| 24 | return sysnand_cache; | ||
| 25 | } | ||
| 26 | |||
| 27 | std::shared_ptr<RegisteredCache> BISFactory::GetUserNANDContents() const { | ||
| 28 | return usrnand_cache; | ||
| 29 | } | ||
| 30 | |||
| 31 | } // namespace FileSys | ||
diff --git a/src/core/file_sys/bis_factory.h b/src/core/file_sys/bis_factory.h new file mode 100644 index 000000000..a970a5e2e --- /dev/null +++ b/src/core/file_sys/bis_factory.h | |||
| @@ -0,0 +1,30 @@ | |||
| 1 | // Copyright 2018 yuzu emulator team | ||
| 2 | // Licensed under GPLv2 or any later version | ||
| 3 | // Refer to the license.txt file included. | ||
| 4 | |||
| 5 | #pragma once | ||
| 6 | |||
| 7 | #include <memory> | ||
| 8 | #include "core/loader/loader.h" | ||
| 9 | #include "registered_cache.h" | ||
| 10 | |||
| 11 | namespace FileSys { | ||
| 12 | |||
| 13 | /// File system interface to the Built-In Storage | ||
| 14 | /// This is currently missing accessors to BIS partitions, but seemed like a good place for the NAND | ||
| 15 | /// registered caches. | ||
| 16 | class BISFactory { | ||
| 17 | public: | ||
| 18 | explicit BISFactory(VirtualDir nand_root); | ||
| 19 | |||
| 20 | std::shared_ptr<RegisteredCache> GetSystemNANDContents() const; | ||
| 21 | std::shared_ptr<RegisteredCache> GetUserNANDContents() const; | ||
| 22 | |||
| 23 | private: | ||
| 24 | VirtualDir nand_root; | ||
| 25 | |||
| 26 | std::shared_ptr<RegisteredCache> sysnand_cache; | ||
| 27 | std::shared_ptr<RegisteredCache> usrnand_cache; | ||
| 28 | }; | ||
| 29 | |||
| 30 | } // namespace FileSys | ||
diff --git a/src/core/file_sys/card_image.cpp b/src/core/file_sys/card_image.cpp index 060948f9e..1d7c7fb10 100644 --- a/src/core/file_sys/card_image.cpp +++ b/src/core/file_sys/card_image.cpp | |||
| @@ -96,6 +96,10 @@ VirtualDir XCI::GetLogoPartition() const { | |||
| 96 | return GetPartition(XCIPartition::Logo); | 96 | return GetPartition(XCIPartition::Logo); |
| 97 | } | 97 | } |
| 98 | 98 | ||
| 99 | const std::vector<std::shared_ptr<NCA>>& XCI::GetNCAs() const { | ||
| 100 | return ncas; | ||
| 101 | } | ||
| 102 | |||
| 99 | std::shared_ptr<NCA> XCI::GetNCAByType(NCAContentType type) const { | 103 | std::shared_ptr<NCA> XCI::GetNCAByType(NCAContentType type) const { |
| 100 | const auto iter = | 104 | const auto iter = |
| 101 | std::find_if(ncas.begin(), ncas.end(), | 105 | std::find_if(ncas.begin(), ncas.end(), |
diff --git a/src/core/file_sys/card_image.h b/src/core/file_sys/card_image.h index 4618d9c00..a03d5264e 100644 --- a/src/core/file_sys/card_image.h +++ b/src/core/file_sys/card_image.h | |||
| @@ -68,6 +68,7 @@ public: | |||
| 68 | VirtualDir GetUpdatePartition() const; | 68 | VirtualDir GetUpdatePartition() const; |
| 69 | VirtualDir GetLogoPartition() const; | 69 | VirtualDir GetLogoPartition() const; |
| 70 | 70 | ||
| 71 | const std::vector<std::shared_ptr<NCA>>& GetNCAs() const; | ||
| 71 | std::shared_ptr<NCA> GetNCAByType(NCAContentType type) const; | 72 | std::shared_ptr<NCA> GetNCAByType(NCAContentType type) const; |
| 72 | VirtualFile GetNCAFileByType(NCAContentType type) const; | 73 | VirtualFile GetNCAFileByType(NCAContentType type) const; |
| 73 | 74 | ||
diff --git a/src/core/file_sys/control_metadata.cpp b/src/core/file_sys/control_metadata.cpp index 3ddc9f162..ae21ad5b9 100644 --- a/src/core/file_sys/control_metadata.cpp +++ b/src/core/file_sys/control_metadata.cpp | |||
| @@ -16,7 +16,7 @@ std::string LanguageEntry::GetDeveloperName() const { | |||
| 16 | return Common::StringFromFixedZeroTerminatedBuffer(developer_name.data(), 0x100); | 16 | return Common::StringFromFixedZeroTerminatedBuffer(developer_name.data(), 0x100); |
| 17 | } | 17 | } |
| 18 | 18 | ||
| 19 | NACP::NACP(VirtualFile file_) : file(std::move(file_)), raw(std::make_unique<RawNACP>()) { | 19 | NACP::NACP(VirtualFile file) : raw(std::make_unique<RawNACP>()) { |
| 20 | file->ReadObject(raw.get()); | 20 | file->ReadObject(raw.get()); |
| 21 | } | 21 | } |
| 22 | 22 | ||
diff --git a/src/core/file_sys/control_metadata.h b/src/core/file_sys/control_metadata.h index 6582cc240..8c2cc1a2a 100644 --- a/src/core/file_sys/control_metadata.h +++ b/src/core/file_sys/control_metadata.h | |||
| @@ -81,7 +81,6 @@ public: | |||
| 81 | std::string GetVersionString() const; | 81 | std::string GetVersionString() const; |
| 82 | 82 | ||
| 83 | private: | 83 | private: |
| 84 | VirtualFile file; | ||
| 85 | std::unique_ptr<RawNACP> raw; | 84 | std::unique_ptr<RawNACP> raw; |
| 86 | }; | 85 | }; |
| 87 | 86 | ||
diff --git a/src/core/file_sys/nca_metadata.cpp b/src/core/file_sys/nca_metadata.cpp new file mode 100644 index 000000000..449244444 --- /dev/null +++ b/src/core/file_sys/nca_metadata.cpp | |||
| @@ -0,0 +1,131 @@ | |||
| 1 | // Copyright 2018 yuzu emulator team | ||
| 2 | // Licensed under GPLv2 or any later version | ||
| 3 | // Refer to the license.txt file included. | ||
| 4 | |||
| 5 | #include <cstring> | ||
| 6 | #include "common/common_funcs.h" | ||
| 7 | #include "common/logging/log.h" | ||
| 8 | #include "common/swap.h" | ||
| 9 | #include "content_archive.h" | ||
| 10 | #include "core/file_sys/nca_metadata.h" | ||
| 11 | |||
| 12 | namespace FileSys { | ||
| 13 | |||
| 14 | bool operator>=(TitleType lhs, TitleType rhs) { | ||
| 15 | return static_cast<size_t>(lhs) >= static_cast<size_t>(rhs); | ||
| 16 | } | ||
| 17 | |||
| 18 | bool operator<=(TitleType lhs, TitleType rhs) { | ||
| 19 | return static_cast<size_t>(lhs) <= static_cast<size_t>(rhs); | ||
| 20 | } | ||
| 21 | |||
| 22 | CNMT::CNMT(VirtualFile file) { | ||
| 23 | if (file->ReadObject(&header) != sizeof(CNMTHeader)) | ||
| 24 | return; | ||
| 25 | |||
| 26 | // If type is {Application, Update, AOC} has opt-header. | ||
| 27 | if (header.type >= TitleType::Application && header.type <= TitleType::AOC) { | ||
| 28 | if (file->ReadObject(&opt_header, sizeof(CNMTHeader)) != sizeof(OptionalHeader)) { | ||
| 29 | LOG_WARNING(Loader, "Failed to read optional header."); | ||
| 30 | } | ||
| 31 | } | ||
| 32 | |||
| 33 | for (u16 i = 0; i < header.number_content_entries; ++i) { | ||
| 34 | auto& next = content_records.emplace_back(ContentRecord{}); | ||
| 35 | if (file->ReadObject(&next, sizeof(CNMTHeader) + i * sizeof(ContentRecord) + | ||
| 36 | header.table_offset) != sizeof(ContentRecord)) { | ||
| 37 | content_records.erase(content_records.end() - 1); | ||
| 38 | } | ||
| 39 | } | ||
| 40 | |||
| 41 | for (u16 i = 0; i < header.number_meta_entries; ++i) { | ||
| 42 | auto& next = meta_records.emplace_back(MetaRecord{}); | ||
| 43 | if (file->ReadObject(&next, sizeof(CNMTHeader) + i * sizeof(MetaRecord) + | ||
| 44 | header.table_offset) != sizeof(MetaRecord)) { | ||
| 45 | meta_records.erase(meta_records.end() - 1); | ||
| 46 | } | ||
| 47 | } | ||
| 48 | } | ||
| 49 | |||
| 50 | CNMT::CNMT(CNMTHeader header, OptionalHeader opt_header, std::vector<ContentRecord> content_records, | ||
| 51 | std::vector<MetaRecord> meta_records) | ||
| 52 | : header(std::move(header)), opt_header(std::move(opt_header)), | ||
| 53 | content_records(std::move(content_records)), meta_records(std::move(meta_records)) {} | ||
| 54 | |||
| 55 | u64 CNMT::GetTitleID() const { | ||
| 56 | return header.title_id; | ||
| 57 | } | ||
| 58 | |||
| 59 | u32 CNMT::GetTitleVersion() const { | ||
| 60 | return header.title_version; | ||
| 61 | } | ||
| 62 | |||
| 63 | TitleType CNMT::GetType() const { | ||
| 64 | return header.type; | ||
| 65 | } | ||
| 66 | |||
| 67 | const std::vector<ContentRecord>& CNMT::GetContentRecords() const { | ||
| 68 | return content_records; | ||
| 69 | } | ||
| 70 | |||
| 71 | const std::vector<MetaRecord>& CNMT::GetMetaRecords() const { | ||
| 72 | return meta_records; | ||
| 73 | } | ||
| 74 | |||
| 75 | bool CNMT::UnionRecords(const CNMT& other) { | ||
| 76 | bool change = false; | ||
| 77 | for (const auto& rec : other.content_records) { | ||
| 78 | const auto iter = std::find_if(content_records.begin(), content_records.end(), | ||
| 79 | [&rec](const ContentRecord& r) { | ||
| 80 | return r.nca_id == rec.nca_id && r.type == rec.type; | ||
| 81 | }); | ||
| 82 | if (iter == content_records.end()) { | ||
| 83 | content_records.emplace_back(rec); | ||
| 84 | ++header.number_content_entries; | ||
| 85 | change = true; | ||
| 86 | } | ||
| 87 | } | ||
| 88 | for (const auto& rec : other.meta_records) { | ||
| 89 | const auto iter = | ||
| 90 | std::find_if(meta_records.begin(), meta_records.end(), [&rec](const MetaRecord& r) { | ||
| 91 | return r.title_id == rec.title_id && r.title_version == rec.title_version && | ||
| 92 | r.type == rec.type; | ||
| 93 | }); | ||
| 94 | if (iter == meta_records.end()) { | ||
| 95 | meta_records.emplace_back(rec); | ||
| 96 | ++header.number_meta_entries; | ||
| 97 | change = true; | ||
| 98 | } | ||
| 99 | } | ||
| 100 | return change; | ||
| 101 | } | ||
| 102 | |||
| 103 | std::vector<u8> CNMT::Serialize() const { | ||
| 104 | const bool has_opt_header = | ||
| 105 | header.type >= TitleType::Application && header.type <= TitleType::AOC; | ||
| 106 | const auto dead_zone = header.table_offset + sizeof(CNMTHeader); | ||
| 107 | std::vector<u8> out( | ||
| 108 | std::max(sizeof(CNMTHeader) + (has_opt_header ? sizeof(OptionalHeader) : 0), dead_zone) + | ||
| 109 | content_records.size() * sizeof(ContentRecord) + meta_records.size() * sizeof(MetaRecord)); | ||
| 110 | memcpy(out.data(), &header, sizeof(CNMTHeader)); | ||
| 111 | |||
| 112 | // Optional Header | ||
| 113 | if (has_opt_header) { | ||
| 114 | memcpy(out.data() + sizeof(CNMTHeader), &opt_header, sizeof(OptionalHeader)); | ||
| 115 | } | ||
| 116 | |||
| 117 | auto offset = header.table_offset; | ||
| 118 | |||
| 119 | for (const auto& rec : content_records) { | ||
| 120 | memcpy(out.data() + offset + sizeof(CNMTHeader), &rec, sizeof(ContentRecord)); | ||
| 121 | offset += sizeof(ContentRecord); | ||
| 122 | } | ||
| 123 | |||
| 124 | for (const auto& rec : meta_records) { | ||
| 125 | memcpy(out.data() + offset + sizeof(CNMTHeader), &rec, sizeof(MetaRecord)); | ||
| 126 | offset += sizeof(MetaRecord); | ||
| 127 | } | ||
| 128 | |||
| 129 | return out; | ||
| 130 | } | ||
| 131 | } // namespace FileSys | ||
diff --git a/src/core/file_sys/nca_metadata.h b/src/core/file_sys/nca_metadata.h new file mode 100644 index 000000000..88e66d4da --- /dev/null +++ b/src/core/file_sys/nca_metadata.h | |||
| @@ -0,0 +1,111 @@ | |||
| 1 | // Copyright 2018 yuzu emulator team | ||
| 2 | // Licensed under GPLv2 or any later version | ||
| 3 | // Refer to the license.txt file included. | ||
| 4 | |||
| 5 | #pragma once | ||
| 6 | |||
| 7 | #include <cstring> | ||
| 8 | #include <memory> | ||
| 9 | #include <vector> | ||
| 10 | #include "common/common_types.h" | ||
| 11 | #include "common/swap.h" | ||
| 12 | #include "core/file_sys/vfs.h" | ||
| 13 | |||
| 14 | namespace FileSys { | ||
| 15 | class CNMT; | ||
| 16 | |||
| 17 | struct CNMTHeader; | ||
| 18 | struct OptionalHeader; | ||
| 19 | |||
| 20 | enum class TitleType : u8 { | ||
| 21 | SystemProgram = 0x01, | ||
| 22 | SystemDataArchive = 0x02, | ||
| 23 | SystemUpdate = 0x03, | ||
| 24 | FirmwarePackageA = 0x04, | ||
| 25 | FirmwarePackageB = 0x05, | ||
| 26 | Application = 0x80, | ||
| 27 | Update = 0x81, | ||
| 28 | AOC = 0x82, | ||
| 29 | DeltaTitle = 0x83, | ||
| 30 | }; | ||
| 31 | |||
| 32 | bool operator>=(TitleType lhs, TitleType rhs); | ||
| 33 | bool operator<=(TitleType lhs, TitleType rhs); | ||
| 34 | |||
| 35 | enum class ContentRecordType : u8 { | ||
| 36 | Meta = 0, | ||
| 37 | Program = 1, | ||
| 38 | Data = 2, | ||
| 39 | Control = 3, | ||
| 40 | Manual = 4, | ||
| 41 | Legal = 5, | ||
| 42 | Patch = 6, | ||
| 43 | }; | ||
| 44 | |||
| 45 | struct ContentRecord { | ||
| 46 | std::array<u8, 0x20> hash; | ||
| 47 | std::array<u8, 0x10> nca_id; | ||
| 48 | std::array<u8, 0x6> size; | ||
| 49 | ContentRecordType type; | ||
| 50 | INSERT_PADDING_BYTES(1); | ||
| 51 | }; | ||
| 52 | static_assert(sizeof(ContentRecord) == 0x38, "ContentRecord has incorrect size."); | ||
| 53 | |||
| 54 | constexpr ContentRecord EMPTY_META_CONTENT_RECORD{{}, {}, {}, ContentRecordType::Meta, {}}; | ||
| 55 | |||
| 56 | struct MetaRecord { | ||
| 57 | u64_le title_id; | ||
| 58 | u32_le title_version; | ||
| 59 | TitleType type; | ||
| 60 | u8 install_byte; | ||
| 61 | INSERT_PADDING_BYTES(2); | ||
| 62 | }; | ||
| 63 | static_assert(sizeof(MetaRecord) == 0x10, "MetaRecord has incorrect size."); | ||
| 64 | |||
| 65 | struct OptionalHeader { | ||
| 66 | u64_le title_id; | ||
| 67 | u64_le minimum_version; | ||
| 68 | }; | ||
| 69 | static_assert(sizeof(OptionalHeader) == 0x10, "OptionalHeader has incorrect size."); | ||
| 70 | |||
| 71 | struct CNMTHeader { | ||
| 72 | u64_le title_id; | ||
| 73 | u32_le title_version; | ||
| 74 | TitleType type; | ||
| 75 | INSERT_PADDING_BYTES(1); | ||
| 76 | u16_le table_offset; | ||
| 77 | u16_le number_content_entries; | ||
| 78 | u16_le number_meta_entries; | ||
| 79 | INSERT_PADDING_BYTES(12); | ||
| 80 | }; | ||
| 81 | static_assert(sizeof(CNMTHeader) == 0x20, "CNMTHeader has incorrect size."); | ||
| 82 | |||
| 83 | // A class representing the format used by NCA metadata files, typically named {}.cnmt.nca or | ||
| 84 | // meta0.ncd. These describe which NCA's belong with which titles in the registered cache. | ||
| 85 | class CNMT { | ||
| 86 | public: | ||
| 87 | explicit CNMT(VirtualFile file); | ||
| 88 | CNMT(CNMTHeader header, OptionalHeader opt_header, std::vector<ContentRecord> content_records, | ||
| 89 | std::vector<MetaRecord> meta_records); | ||
| 90 | |||
| 91 | u64 GetTitleID() const; | ||
| 92 | u32 GetTitleVersion() const; | ||
| 93 | TitleType GetType() const; | ||
| 94 | |||
| 95 | const std::vector<ContentRecord>& GetContentRecords() const; | ||
| 96 | const std::vector<MetaRecord>& GetMetaRecords() const; | ||
| 97 | |||
| 98 | bool UnionRecords(const CNMT& other); | ||
| 99 | std::vector<u8> Serialize() const; | ||
| 100 | |||
| 101 | private: | ||
| 102 | CNMTHeader header; | ||
| 103 | OptionalHeader opt_header; | ||
| 104 | std::vector<ContentRecord> content_records; | ||
| 105 | std::vector<MetaRecord> meta_records; | ||
| 106 | |||
| 107 | // TODO(DarkLordZach): According to switchbrew, for Patch-type there is additional data | ||
| 108 | // after the table. This is not documented, unfortunately. | ||
| 109 | }; | ||
| 110 | |||
| 111 | } // namespace FileSys | ||
diff --git a/src/core/file_sys/registered_cache.cpp b/src/core/file_sys/registered_cache.cpp new file mode 100644 index 000000000..a5e935f64 --- /dev/null +++ b/src/core/file_sys/registered_cache.cpp | |||
| @@ -0,0 +1,476 @@ | |||
| 1 | // Copyright 2018 yuzu emulator team | ||
| 2 | // Licensed under GPLv2 or any later version | ||
| 3 | // Refer to the license.txt file included. | ||
| 4 | |||
| 5 | #include <regex> | ||
| 6 | #include <mbedtls/sha256.h> | ||
| 7 | #include "common/assert.h" | ||
| 8 | #include "common/hex_util.h" | ||
| 9 | #include "common/logging/log.h" | ||
| 10 | #include "core/crypto/encryption_layer.h" | ||
| 11 | #include "core/file_sys/card_image.h" | ||
| 12 | #include "core/file_sys/nca_metadata.h" | ||
| 13 | #include "core/file_sys/registered_cache.h" | ||
| 14 | #include "core/file_sys/vfs_concat.h" | ||
| 15 | |||
| 16 | namespace FileSys { | ||
| 17 | std::string RegisteredCacheEntry::DebugInfo() const { | ||
| 18 | return fmt::format("title_id={:016X}, content_type={:02X}", title_id, static_cast<u8>(type)); | ||
| 19 | } | ||
| 20 | |||
| 21 | bool operator<(const RegisteredCacheEntry& lhs, const RegisteredCacheEntry& rhs) { | ||
| 22 | return (lhs.title_id < rhs.title_id) || (lhs.title_id == rhs.title_id && lhs.type < rhs.type); | ||
| 23 | } | ||
| 24 | |||
| 25 | static bool FollowsTwoDigitDirFormat(std::string_view name) { | ||
| 26 | static const std::regex two_digit_regex("000000[0-9A-F]{2}", std::regex_constants::ECMAScript | | ||
| 27 | std::regex_constants::icase); | ||
| 28 | return std::regex_match(name.begin(), name.end(), two_digit_regex); | ||
| 29 | } | ||
| 30 | |||
| 31 | static bool FollowsNcaIdFormat(std::string_view name) { | ||
| 32 | static const std::regex nca_id_regex("[0-9A-F]{32}\\.nca", std::regex_constants::ECMAScript | | ||
| 33 | std::regex_constants::icase); | ||
| 34 | return name.size() == 36 && std::regex_match(name.begin(), name.end(), nca_id_regex); | ||
| 35 | } | ||
| 36 | |||
| 37 | static std::string GetRelativePathFromNcaID(const std::array<u8, 16>& nca_id, bool second_hex_upper, | ||
| 38 | bool within_two_digit) { | ||
| 39 | if (!within_two_digit) | ||
| 40 | return fmt::format("/{}.nca", HexArrayToString(nca_id, second_hex_upper)); | ||
| 41 | |||
| 42 | Core::Crypto::SHA256Hash hash{}; | ||
| 43 | mbedtls_sha256(nca_id.data(), nca_id.size(), hash.data(), 0); | ||
| 44 | return fmt::format("/000000{:02X}/{}.nca", hash[0], HexArrayToString(nca_id, second_hex_upper)); | ||
| 45 | } | ||
| 46 | |||
| 47 | static std::string GetCNMTName(TitleType type, u64 title_id) { | ||
| 48 | constexpr std::array<const char*, 9> TITLE_TYPE_NAMES{ | ||
| 49 | "SystemProgram", | ||
| 50 | "SystemData", | ||
| 51 | "SystemUpdate", | ||
| 52 | "BootImagePackage", | ||
| 53 | "BootImagePackageSafe", | ||
| 54 | "Application", | ||
| 55 | "Patch", | ||
| 56 | "AddOnContent", | ||
| 57 | "" ///< Currently unknown 'DeltaTitle' | ||
| 58 | }; | ||
| 59 | |||
| 60 | auto index = static_cast<size_t>(type); | ||
| 61 | // If the index is after the jump in TitleType, subtract it out. | ||
| 62 | if (index >= static_cast<size_t>(TitleType::Application)) { | ||
| 63 | index -= static_cast<size_t>(TitleType::Application) - | ||
| 64 | static_cast<size_t>(TitleType::FirmwarePackageB); | ||
| 65 | } | ||
| 66 | return fmt::format("{}_{:016x}.cnmt", TITLE_TYPE_NAMES[index], title_id); | ||
| 67 | } | ||
| 68 | |||
| 69 | static ContentRecordType GetCRTypeFromNCAType(NCAContentType type) { | ||
| 70 | switch (type) { | ||
| 71 | case NCAContentType::Program: | ||
| 72 | // TODO(DarkLordZach): Differentiate between Program and Patch | ||
| 73 | return ContentRecordType::Program; | ||
| 74 | case NCAContentType::Meta: | ||
| 75 | return ContentRecordType::Meta; | ||
| 76 | case NCAContentType::Control: | ||
| 77 | return ContentRecordType::Control; | ||
| 78 | case NCAContentType::Data: | ||
| 79 | return ContentRecordType::Data; | ||
| 80 | case NCAContentType::Manual: | ||
| 81 | // TODO(DarkLordZach): Peek at NCA contents to differentiate Manual and Legal. | ||
| 82 | return ContentRecordType::Manual; | ||
| 83 | default: | ||
| 84 | UNREACHABLE(); | ||
| 85 | } | ||
| 86 | } | ||
| 87 | |||
| 88 | VirtualFile RegisteredCache::OpenFileOrDirectoryConcat(const VirtualDir& dir, | ||
| 89 | std::string_view path) const { | ||
| 90 | if (dir->GetFileRelative(path) != nullptr) | ||
| 91 | return dir->GetFileRelative(path); | ||
| 92 | if (dir->GetDirectoryRelative(path) != nullptr) { | ||
| 93 | const auto nca_dir = dir->GetDirectoryRelative(path); | ||
| 94 | VirtualFile file = nullptr; | ||
| 95 | |||
| 96 | const auto files = nca_dir->GetFiles(); | ||
| 97 | if (files.size() == 1 && files[0]->GetName() == "00") { | ||
| 98 | file = files[0]; | ||
| 99 | } else { | ||
| 100 | std::vector<VirtualFile> concat; | ||
| 101 | // Since the files are a two-digit hex number, max is FF. | ||
| 102 | for (size_t i = 0; i < 0x100; ++i) { | ||
| 103 | auto next = nca_dir->GetFile(fmt::format("{:02X}", i)); | ||
| 104 | if (next != nullptr) { | ||
| 105 | concat.push_back(std::move(next)); | ||
| 106 | } else { | ||
| 107 | next = nca_dir->GetFile(fmt::format("{:02x}", i)); | ||
| 108 | if (next != nullptr) | ||
| 109 | concat.push_back(std::move(next)); | ||
| 110 | else | ||
| 111 | break; | ||
| 112 | } | ||
| 113 | } | ||
| 114 | |||
| 115 | if (concat.empty()) | ||
| 116 | return nullptr; | ||
| 117 | |||
| 118 | file = FileSys::ConcatenateFiles(concat); | ||
| 119 | } | ||
| 120 | |||
| 121 | return file; | ||
| 122 | } | ||
| 123 | return nullptr; | ||
| 124 | } | ||
| 125 | |||
| 126 | VirtualFile RegisteredCache::GetFileAtID(NcaID id) const { | ||
| 127 | VirtualFile file; | ||
| 128 | // Try all four modes of file storage: | ||
| 129 | // (bit 1 = uppercase/lower, bit 0 = within a two-digit dir) | ||
| 130 | // 00: /000000**/{:032X}.nca | ||
| 131 | // 01: /{:032X}.nca | ||
| 132 | // 10: /000000**/{:032x}.nca | ||
| 133 | // 11: /{:032x}.nca | ||
| 134 | for (u8 i = 0; i < 4; ++i) { | ||
| 135 | const auto path = GetRelativePathFromNcaID(id, (i & 0b10) == 0, (i & 0b01) == 0); | ||
| 136 | file = OpenFileOrDirectoryConcat(dir, path); | ||
| 137 | if (file != nullptr) | ||
| 138 | return file; | ||
| 139 | } | ||
| 140 | return file; | ||
| 141 | } | ||
| 142 | |||
| 143 | static boost::optional<NcaID> CheckMapForContentRecord( | ||
| 144 | const boost::container::flat_map<u64, CNMT>& map, u64 title_id, ContentRecordType type) { | ||
| 145 | if (map.find(title_id) == map.end()) | ||
| 146 | return boost::none; | ||
| 147 | |||
| 148 | const auto& cnmt = map.at(title_id); | ||
| 149 | |||
| 150 | const auto iter = std::find_if(cnmt.GetContentRecords().begin(), cnmt.GetContentRecords().end(), | ||
| 151 | [type](const ContentRecord& rec) { return rec.type == type; }); | ||
| 152 | if (iter == cnmt.GetContentRecords().end()) | ||
| 153 | return boost::none; | ||
| 154 | |||
| 155 | return boost::make_optional(iter->nca_id); | ||
| 156 | } | ||
| 157 | |||
| 158 | boost::optional<NcaID> RegisteredCache::GetNcaIDFromMetadata(u64 title_id, | ||
| 159 | ContentRecordType type) const { | ||
| 160 | if (type == ContentRecordType::Meta && meta_id.find(title_id) != meta_id.end()) | ||
| 161 | return meta_id.at(title_id); | ||
| 162 | |||
| 163 | const auto res1 = CheckMapForContentRecord(yuzu_meta, title_id, type); | ||
| 164 | if (res1 != boost::none) | ||
| 165 | return res1; | ||
| 166 | return CheckMapForContentRecord(meta, title_id, type); | ||
| 167 | } | ||
| 168 | |||
| 169 | std::vector<NcaID> RegisteredCache::AccumulateFiles() const { | ||
| 170 | std::vector<NcaID> ids; | ||
| 171 | for (const auto& d2_dir : dir->GetSubdirectories()) { | ||
| 172 | if (FollowsNcaIdFormat(d2_dir->GetName())) { | ||
| 173 | ids.push_back(HexStringToArray<0x10, true>(d2_dir->GetName().substr(0, 0x20))); | ||
| 174 | continue; | ||
| 175 | } | ||
| 176 | |||
| 177 | if (!FollowsTwoDigitDirFormat(d2_dir->GetName())) | ||
| 178 | continue; | ||
| 179 | |||
| 180 | for (const auto& nca_dir : d2_dir->GetSubdirectories()) { | ||
| 181 | if (!FollowsNcaIdFormat(nca_dir->GetName())) | ||
| 182 | continue; | ||
| 183 | |||
| 184 | ids.push_back(HexStringToArray<0x10, true>(nca_dir->GetName().substr(0, 0x20))); | ||
| 185 | } | ||
| 186 | |||
| 187 | for (const auto& nca_file : d2_dir->GetFiles()) { | ||
| 188 | if (!FollowsNcaIdFormat(nca_file->GetName())) | ||
| 189 | continue; | ||
| 190 | |||
| 191 | ids.push_back(HexStringToArray<0x10, true>(nca_file->GetName().substr(0, 0x20))); | ||
| 192 | } | ||
| 193 | } | ||
| 194 | |||
| 195 | for (const auto& d2_file : dir->GetFiles()) { | ||
| 196 | if (FollowsNcaIdFormat(d2_file->GetName())) | ||
| 197 | ids.push_back(HexStringToArray<0x10, true>(d2_file->GetName().substr(0, 0x20))); | ||
| 198 | } | ||
| 199 | return ids; | ||
| 200 | } | ||
| 201 | |||
| 202 | void RegisteredCache::ProcessFiles(const std::vector<NcaID>& ids) { | ||
| 203 | for (const auto& id : ids) { | ||
| 204 | const auto file = GetFileAtID(id); | ||
| 205 | |||
| 206 | if (file == nullptr) | ||
| 207 | continue; | ||
| 208 | const auto nca = std::make_shared<NCA>(parser(file, id)); | ||
| 209 | if (nca->GetStatus() != Loader::ResultStatus::Success || | ||
| 210 | nca->GetType() != NCAContentType::Meta) { | ||
| 211 | continue; | ||
| 212 | } | ||
| 213 | |||
| 214 | const auto section0 = nca->GetSubdirectories()[0]; | ||
| 215 | |||
| 216 | for (const auto& file : section0->GetFiles()) { | ||
| 217 | if (file->GetExtension() != "cnmt") | ||
| 218 | continue; | ||
| 219 | |||
| 220 | meta.insert_or_assign(nca->GetTitleId(), CNMT(file)); | ||
| 221 | meta_id.insert_or_assign(nca->GetTitleId(), id); | ||
| 222 | break; | ||
| 223 | } | ||
| 224 | } | ||
| 225 | } | ||
| 226 | |||
| 227 | void RegisteredCache::AccumulateYuzuMeta() { | ||
| 228 | const auto dir = this->dir->GetSubdirectory("yuzu_meta"); | ||
| 229 | if (dir == nullptr) | ||
| 230 | return; | ||
| 231 | |||
| 232 | for (const auto& file : dir->GetFiles()) { | ||
| 233 | if (file->GetExtension() != "cnmt") | ||
| 234 | continue; | ||
| 235 | |||
| 236 | CNMT cnmt(file); | ||
| 237 | yuzu_meta.insert_or_assign(cnmt.GetTitleID(), std::move(cnmt)); | ||
| 238 | } | ||
| 239 | } | ||
| 240 | |||
| 241 | void RegisteredCache::Refresh() { | ||
| 242 | if (dir == nullptr) | ||
| 243 | return; | ||
| 244 | const auto ids = AccumulateFiles(); | ||
| 245 | ProcessFiles(ids); | ||
| 246 | AccumulateYuzuMeta(); | ||
| 247 | } | ||
| 248 | |||
| 249 | RegisteredCache::RegisteredCache(VirtualDir dir_, RegisteredCacheParsingFunction parsing_function) | ||
| 250 | : dir(std::move(dir_)), parser(std::move(parsing_function)) { | ||
| 251 | Refresh(); | ||
| 252 | } | ||
| 253 | |||
| 254 | bool RegisteredCache::HasEntry(u64 title_id, ContentRecordType type) const { | ||
| 255 | return GetEntryRaw(title_id, type) != nullptr; | ||
| 256 | } | ||
| 257 | |||
| 258 | bool RegisteredCache::HasEntry(RegisteredCacheEntry entry) const { | ||
| 259 | return GetEntryRaw(entry) != nullptr; | ||
| 260 | } | ||
| 261 | |||
| 262 | VirtualFile RegisteredCache::GetEntryRaw(u64 title_id, ContentRecordType type) const { | ||
| 263 | const auto id = GetNcaIDFromMetadata(title_id, type); | ||
| 264 | if (id == boost::none) | ||
| 265 | return nullptr; | ||
| 266 | |||
| 267 | return parser(GetFileAtID(id.get()), id.get()); | ||
| 268 | } | ||
| 269 | |||
| 270 | VirtualFile RegisteredCache::GetEntryRaw(RegisteredCacheEntry entry) const { | ||
| 271 | return GetEntryRaw(entry.title_id, entry.type); | ||
| 272 | } | ||
| 273 | |||
| 274 | std::shared_ptr<NCA> RegisteredCache::GetEntry(u64 title_id, ContentRecordType type) const { | ||
| 275 | const auto raw = GetEntryRaw(title_id, type); | ||
| 276 | if (raw == nullptr) | ||
| 277 | return nullptr; | ||
| 278 | return std::make_shared<NCA>(raw); | ||
| 279 | } | ||
| 280 | |||
| 281 | std::shared_ptr<NCA> RegisteredCache::GetEntry(RegisteredCacheEntry entry) const { | ||
| 282 | return GetEntry(entry.title_id, entry.type); | ||
| 283 | } | ||
| 284 | |||
| 285 | template <typename T> | ||
| 286 | void RegisteredCache::IterateAllMetadata( | ||
| 287 | std::vector<T>& out, std::function<T(const CNMT&, const ContentRecord&)> proc, | ||
| 288 | std::function<bool(const CNMT&, const ContentRecord&)> filter) const { | ||
| 289 | for (const auto& kv : meta) { | ||
| 290 | const auto& cnmt = kv.second; | ||
| 291 | if (filter(cnmt, EMPTY_META_CONTENT_RECORD)) | ||
| 292 | out.push_back(proc(cnmt, EMPTY_META_CONTENT_RECORD)); | ||
| 293 | for (const auto& rec : cnmt.GetContentRecords()) { | ||
| 294 | if (GetFileAtID(rec.nca_id) != nullptr && filter(cnmt, rec)) { | ||
| 295 | out.push_back(proc(cnmt, rec)); | ||
| 296 | } | ||
| 297 | } | ||
| 298 | } | ||
| 299 | for (const auto& kv : yuzu_meta) { | ||
| 300 | const auto& cnmt = kv.second; | ||
| 301 | for (const auto& rec : cnmt.GetContentRecords()) { | ||
| 302 | if (GetFileAtID(rec.nca_id) != nullptr && filter(cnmt, rec)) { | ||
| 303 | out.push_back(proc(cnmt, rec)); | ||
| 304 | } | ||
| 305 | } | ||
| 306 | } | ||
| 307 | } | ||
| 308 | |||
| 309 | std::vector<RegisteredCacheEntry> RegisteredCache::ListEntries() const { | ||
| 310 | std::vector<RegisteredCacheEntry> out; | ||
| 311 | IterateAllMetadata<RegisteredCacheEntry>( | ||
| 312 | out, | ||
| 313 | [](const CNMT& c, const ContentRecord& r) { | ||
| 314 | return RegisteredCacheEntry{c.GetTitleID(), r.type}; | ||
| 315 | }, | ||
| 316 | [](const CNMT& c, const ContentRecord& r) { return true; }); | ||
| 317 | return out; | ||
| 318 | } | ||
| 319 | |||
| 320 | std::vector<RegisteredCacheEntry> RegisteredCache::ListEntriesFilter( | ||
| 321 | boost::optional<TitleType> title_type, boost::optional<ContentRecordType> record_type, | ||
| 322 | boost::optional<u64> title_id) const { | ||
| 323 | std::vector<RegisteredCacheEntry> out; | ||
| 324 | IterateAllMetadata<RegisteredCacheEntry>( | ||
| 325 | out, | ||
| 326 | [](const CNMT& c, const ContentRecord& r) { | ||
| 327 | return RegisteredCacheEntry{c.GetTitleID(), r.type}; | ||
| 328 | }, | ||
| 329 | [&title_type, &record_type, &title_id](const CNMT& c, const ContentRecord& r) { | ||
| 330 | if (title_type != boost::none && title_type.get() != c.GetType()) | ||
| 331 | return false; | ||
| 332 | if (record_type != boost::none && record_type.get() != r.type) | ||
| 333 | return false; | ||
| 334 | if (title_id != boost::none && title_id.get() != c.GetTitleID()) | ||
| 335 | return false; | ||
| 336 | return true; | ||
| 337 | }); | ||
| 338 | return out; | ||
| 339 | } | ||
| 340 | |||
| 341 | static std::shared_ptr<NCA> GetNCAFromXCIForID(std::shared_ptr<XCI> xci, const NcaID& id) { | ||
| 342 | const auto filename = fmt::format("{}.nca", HexArrayToString(id, false)); | ||
| 343 | const auto iter = | ||
| 344 | std::find_if(xci->GetNCAs().begin(), xci->GetNCAs().end(), | ||
| 345 | [&filename](std::shared_ptr<NCA> nca) { return nca->GetName() == filename; }); | ||
| 346 | return iter == xci->GetNCAs().end() ? nullptr : *iter; | ||
| 347 | } | ||
| 348 | |||
| 349 | InstallResult RegisteredCache::InstallEntry(std::shared_ptr<XCI> xci, bool overwrite_if_exists, | ||
| 350 | const VfsCopyFunction& copy) { | ||
| 351 | const auto& ncas = xci->GetNCAs(); | ||
| 352 | const auto& meta_iter = std::find_if(ncas.begin(), ncas.end(), [](std::shared_ptr<NCA> nca) { | ||
| 353 | return nca->GetType() == NCAContentType::Meta; | ||
| 354 | }); | ||
| 355 | |||
| 356 | if (meta_iter == ncas.end()) { | ||
| 357 | LOG_ERROR(Loader, "The XCI you are attempting to install does not have a metadata NCA and " | ||
| 358 | "is therefore malformed. Double check your encryption keys."); | ||
| 359 | return InstallResult::ErrorMetaFailed; | ||
| 360 | } | ||
| 361 | |||
| 362 | // Install Metadata File | ||
| 363 | const auto meta_id_raw = (*meta_iter)->GetName().substr(0, 32); | ||
| 364 | const auto meta_id = HexStringToArray<16>(meta_id_raw); | ||
| 365 | |||
| 366 | const auto res = RawInstallNCA(*meta_iter, copy, overwrite_if_exists, meta_id); | ||
| 367 | if (res != InstallResult::Success) | ||
| 368 | return res; | ||
| 369 | |||
| 370 | // Install all the other NCAs | ||
| 371 | const auto section0 = (*meta_iter)->GetSubdirectories()[0]; | ||
| 372 | const auto cnmt_file = section0->GetFiles()[0]; | ||
| 373 | const CNMT cnmt(cnmt_file); | ||
| 374 | for (const auto& record : cnmt.GetContentRecords()) { | ||
| 375 | const auto nca = GetNCAFromXCIForID(xci, record.nca_id); | ||
| 376 | if (nca == nullptr) | ||
| 377 | return InstallResult::ErrorCopyFailed; | ||
| 378 | const auto res2 = RawInstallNCA(nca, copy, overwrite_if_exists, record.nca_id); | ||
| 379 | if (res2 != InstallResult::Success) | ||
| 380 | return res2; | ||
| 381 | } | ||
| 382 | |||
| 383 | Refresh(); | ||
| 384 | return InstallResult::Success; | ||
| 385 | } | ||
| 386 | |||
| 387 | InstallResult RegisteredCache::InstallEntry(std::shared_ptr<NCA> nca, TitleType type, | ||
| 388 | bool overwrite_if_exists, const VfsCopyFunction& copy) { | ||
| 389 | CNMTHeader header{ | ||
| 390 | nca->GetTitleId(), ///< Title ID | ||
| 391 | 0, ///< Ignore/Default title version | ||
| 392 | type, ///< Type | ||
| 393 | {}, ///< Padding | ||
| 394 | 0x10, ///< Default table offset | ||
| 395 | 1, ///< 1 Content Entry | ||
| 396 | 0, ///< No Meta Entries | ||
| 397 | {}, ///< Padding | ||
| 398 | }; | ||
| 399 | OptionalHeader opt_header{0, 0}; | ||
| 400 | ContentRecord c_rec{{}, {}, {}, GetCRTypeFromNCAType(nca->GetType()), {}}; | ||
| 401 | const auto& data = nca->GetBaseFile()->ReadBytes(0x100000); | ||
| 402 | mbedtls_sha256(data.data(), data.size(), c_rec.hash.data(), 0); | ||
| 403 | memcpy(&c_rec.nca_id, &c_rec.hash, 16); | ||
| 404 | const CNMT new_cnmt(header, opt_header, {c_rec}, {}); | ||
| 405 | if (!RawInstallYuzuMeta(new_cnmt)) | ||
| 406 | return InstallResult::ErrorMetaFailed; | ||
| 407 | return RawInstallNCA(nca, copy, overwrite_if_exists, c_rec.nca_id); | ||
| 408 | } | ||
| 409 | |||
| 410 | InstallResult RegisteredCache::RawInstallNCA(std::shared_ptr<NCA> nca, const VfsCopyFunction& copy, | ||
| 411 | bool overwrite_if_exists, | ||
| 412 | boost::optional<NcaID> override_id) { | ||
| 413 | const auto in = nca->GetBaseFile(); | ||
| 414 | Core::Crypto::SHA256Hash hash{}; | ||
| 415 | |||
| 416 | // Calculate NcaID | ||
| 417 | // NOTE: Because computing the SHA256 of an entire NCA is quite expensive (especially if the | ||
| 418 | // game is massive), we're going to cheat and only hash the first MB of the NCA. | ||
| 419 | // Also, for XCIs the NcaID matters, so if the override id isn't none, use that. | ||
| 420 | NcaID id{}; | ||
| 421 | if (override_id == boost::none) { | ||
| 422 | const auto& data = in->ReadBytes(0x100000); | ||
| 423 | mbedtls_sha256(data.data(), data.size(), hash.data(), 0); | ||
| 424 | memcpy(id.data(), hash.data(), 16); | ||
| 425 | } else { | ||
| 426 | id = override_id.get(); | ||
| 427 | } | ||
| 428 | |||
| 429 | std::string path = GetRelativePathFromNcaID(id, false, true); | ||
| 430 | |||
| 431 | if (GetFileAtID(id) != nullptr && !overwrite_if_exists) { | ||
| 432 | LOG_WARNING(Loader, "Attempting to overwrite existing NCA. Skipping..."); | ||
| 433 | return InstallResult::ErrorAlreadyExists; | ||
| 434 | } | ||
| 435 | |||
| 436 | if (GetFileAtID(id) != nullptr) { | ||
| 437 | LOG_WARNING(Loader, "Overwriting existing NCA..."); | ||
| 438 | VirtualDir c_dir; | ||
| 439 | { c_dir = dir->GetFileRelative(path)->GetContainingDirectory(); } | ||
| 440 | c_dir->DeleteFile(FileUtil::GetFilename(path)); | ||
| 441 | } | ||
| 442 | |||
| 443 | auto out = dir->CreateFileRelative(path); | ||
| 444 | if (out == nullptr) | ||
| 445 | return InstallResult::ErrorCopyFailed; | ||
| 446 | return copy(in, out) ? InstallResult::Success : InstallResult::ErrorCopyFailed; | ||
| 447 | } | ||
| 448 | |||
| 449 | bool RegisteredCache::RawInstallYuzuMeta(const CNMT& cnmt) { | ||
| 450 | // Reasoning behind this method can be found in the comment for InstallEntry, NCA overload. | ||
| 451 | const auto dir = this->dir->CreateDirectoryRelative("yuzu_meta"); | ||
| 452 | const auto filename = GetCNMTName(cnmt.GetType(), cnmt.GetTitleID()); | ||
| 453 | if (dir->GetFile(filename) == nullptr) { | ||
| 454 | auto out = dir->CreateFile(filename); | ||
| 455 | const auto buffer = cnmt.Serialize(); | ||
| 456 | out->Resize(buffer.size()); | ||
| 457 | out->WriteBytes(buffer); | ||
| 458 | } else { | ||
| 459 | auto out = dir->GetFile(filename); | ||
| 460 | CNMT old_cnmt(out); | ||
| 461 | // Returns true on change | ||
| 462 | if (old_cnmt.UnionRecords(cnmt)) { | ||
| 463 | out->Resize(0); | ||
| 464 | const auto buffer = old_cnmt.Serialize(); | ||
| 465 | out->Resize(buffer.size()); | ||
| 466 | out->WriteBytes(buffer); | ||
| 467 | } | ||
| 468 | } | ||
| 469 | Refresh(); | ||
| 470 | return std::find_if(yuzu_meta.begin(), yuzu_meta.end(), | ||
| 471 | [&cnmt](const std::pair<u64, CNMT>& kv) { | ||
| 472 | return kv.second.GetType() == cnmt.GetType() && | ||
| 473 | kv.second.GetTitleID() == cnmt.GetTitleID(); | ||
| 474 | }) != yuzu_meta.end(); | ||
| 475 | } | ||
| 476 | } // namespace FileSys | ||
diff --git a/src/core/file_sys/registered_cache.h b/src/core/file_sys/registered_cache.h new file mode 100644 index 000000000..a7c51a59c --- /dev/null +++ b/src/core/file_sys/registered_cache.h | |||
| @@ -0,0 +1,124 @@ | |||
| 1 | // Copyright 2018 yuzu emulator team | ||
| 2 | // Licensed under GPLv2 or any later version | ||
| 3 | // Refer to the license.txt file included. | ||
| 4 | |||
| 5 | #pragma once | ||
| 6 | |||
| 7 | #include <array> | ||
| 8 | #include <functional> | ||
| 9 | #include <map> | ||
| 10 | #include <memory> | ||
| 11 | #include <string> | ||
| 12 | #include <vector> | ||
| 13 | #include <boost/container/flat_map.hpp> | ||
| 14 | #include "common/common_funcs.h" | ||
| 15 | #include "common/common_types.h" | ||
| 16 | #include "content_archive.h" | ||
| 17 | #include "core/file_sys/nca_metadata.h" | ||
| 18 | #include "core/file_sys/vfs.h" | ||
| 19 | |||
| 20 | namespace FileSys { | ||
| 21 | class XCI; | ||
| 22 | class CNMT; | ||
| 23 | |||
| 24 | using NcaID = std::array<u8, 0x10>; | ||
| 25 | using RegisteredCacheParsingFunction = std::function<VirtualFile(const VirtualFile&, const NcaID&)>; | ||
| 26 | using VfsCopyFunction = std::function<bool(VirtualFile, VirtualFile)>; | ||
| 27 | |||
| 28 | enum class InstallResult { | ||
| 29 | Success, | ||
| 30 | ErrorAlreadyExists, | ||
| 31 | ErrorCopyFailed, | ||
| 32 | ErrorMetaFailed, | ||
| 33 | }; | ||
| 34 | |||
| 35 | struct RegisteredCacheEntry { | ||
| 36 | u64 title_id; | ||
| 37 | ContentRecordType type; | ||
| 38 | |||
| 39 | std::string DebugInfo() const; | ||
| 40 | }; | ||
| 41 | |||
| 42 | // boost flat_map requires operator< for O(log(n)) lookups. | ||
| 43 | bool operator<(const RegisteredCacheEntry& lhs, const RegisteredCacheEntry& rhs); | ||
| 44 | |||
| 45 | /* | ||
| 46 | * A class that catalogues NCAs in the registered directory structure. | ||
| 47 | * Nintendo's registered format follows this structure: | ||
| 48 | * | ||
| 49 | * Root | ||
| 50 | * | 000000XX <- XX is the ____ two digits of the NcaID | ||
| 51 | * | <hash>.nca <- hash is the NcaID (first half of SHA256 over entire file) (folder) | ||
| 52 | * | 00 | ||
| 53 | * | 01 <- Actual content split along 4GB boundaries. (optional) | ||
| 54 | * | ||
| 55 | * (This impl also supports substituting the nca dir for an nca file, as that's more convenient when | ||
| 56 | * 4GB splitting can be ignored.) | ||
| 57 | */ | ||
| 58 | class RegisteredCache { | ||
| 59 | public: | ||
| 60 | // Parsing function defines the conversion from raw file to NCA. If there are other steps | ||
| 61 | // besides creating the NCA from the file (e.g. NAX0 on SD Card), that should go in a custom | ||
| 62 | // parsing function. | ||
| 63 | explicit RegisteredCache(VirtualDir dir, | ||
| 64 | RegisteredCacheParsingFunction parsing_function = | ||
| 65 | [](const VirtualFile& file, const NcaID& id) { return file; }); | ||
| 66 | |||
| 67 | void Refresh(); | ||
| 68 | |||
| 69 | bool HasEntry(u64 title_id, ContentRecordType type) const; | ||
| 70 | bool HasEntry(RegisteredCacheEntry entry) const; | ||
| 71 | |||
| 72 | VirtualFile GetEntryRaw(u64 title_id, ContentRecordType type) const; | ||
| 73 | VirtualFile GetEntryRaw(RegisteredCacheEntry entry) const; | ||
| 74 | |||
| 75 | std::shared_ptr<NCA> GetEntry(u64 title_id, ContentRecordType type) const; | ||
| 76 | std::shared_ptr<NCA> GetEntry(RegisteredCacheEntry entry) const; | ||
| 77 | |||
| 78 | std::vector<RegisteredCacheEntry> ListEntries() const; | ||
| 79 | // If a parameter is not boost::none, it will be filtered for from all entries. | ||
| 80 | std::vector<RegisteredCacheEntry> ListEntriesFilter( | ||
| 81 | boost::optional<TitleType> title_type = boost::none, | ||
| 82 | boost::optional<ContentRecordType> record_type = boost::none, | ||
| 83 | boost::optional<u64> title_id = boost::none) const; | ||
| 84 | |||
| 85 | // Raw copies all the ncas from the xci to the csache. Does some quick checks to make sure there | ||
| 86 | // is a meta NCA and all of them are accessible. | ||
| 87 | InstallResult InstallEntry(std::shared_ptr<XCI> xci, bool overwrite_if_exists = false, | ||
| 88 | const VfsCopyFunction& copy = &VfsRawCopy); | ||
| 89 | |||
| 90 | // Due to the fact that we must use Meta-type NCAs to determine the existance of files, this | ||
| 91 | // poses quite a challenge. Instead of creating a new meta NCA for this file, yuzu will create a | ||
| 92 | // dir inside the NAND called 'yuzu_meta' and store the raw CNMT there. | ||
| 93 | // TODO(DarkLordZach): Author real meta-type NCAs and install those. | ||
| 94 | InstallResult InstallEntry(std::shared_ptr<NCA> nca, TitleType type, | ||
| 95 | bool overwrite_if_exists = false, | ||
| 96 | const VfsCopyFunction& copy = &VfsRawCopy); | ||
| 97 | |||
| 98 | private: | ||
| 99 | template <typename T> | ||
| 100 | void IterateAllMetadata(std::vector<T>& out, | ||
| 101 | std::function<T(const CNMT&, const ContentRecord&)> proc, | ||
| 102 | std::function<bool(const CNMT&, const ContentRecord&)> filter) const; | ||
| 103 | std::vector<NcaID> AccumulateFiles() const; | ||
| 104 | void ProcessFiles(const std::vector<NcaID>& ids); | ||
| 105 | void AccumulateYuzuMeta(); | ||
| 106 | boost::optional<NcaID> GetNcaIDFromMetadata(u64 title_id, ContentRecordType type) const; | ||
| 107 | VirtualFile GetFileAtID(NcaID id) const; | ||
| 108 | VirtualFile OpenFileOrDirectoryConcat(const VirtualDir& dir, std::string_view path) const; | ||
| 109 | InstallResult RawInstallNCA(std::shared_ptr<NCA> nca, const VfsCopyFunction& copy, | ||
| 110 | bool overwrite_if_exists, | ||
| 111 | boost::optional<NcaID> override_id = boost::none); | ||
| 112 | bool RawInstallYuzuMeta(const CNMT& cnmt); | ||
| 113 | |||
| 114 | VirtualDir dir; | ||
| 115 | RegisteredCacheParsingFunction parser; | ||
| 116 | // maps tid -> NcaID of meta | ||
| 117 | boost::container::flat_map<u64, NcaID> meta_id; | ||
| 118 | // maps tid -> meta | ||
| 119 | boost::container::flat_map<u64, CNMT> meta; | ||
| 120 | // maps tid -> meta for CNMT in yuzu_meta | ||
| 121 | boost::container::flat_map<u64, CNMT> yuzu_meta; | ||
| 122 | }; | ||
| 123 | |||
| 124 | } // namespace FileSys | ||
diff --git a/src/core/file_sys/romfs.cpp b/src/core/file_sys/romfs.cpp index ff3ddb29c..e490c8ace 100644 --- a/src/core/file_sys/romfs.cpp +++ b/src/core/file_sys/romfs.cpp | |||
| @@ -65,7 +65,7 @@ void ProcessFile(VirtualFile file, size_t file_offset, size_t data_offset, u32 t | |||
| 65 | auto entry = GetEntry<FileEntry>(file, file_offset + this_file_offset); | 65 | auto entry = GetEntry<FileEntry>(file, file_offset + this_file_offset); |
| 66 | 66 | ||
| 67 | parent->AddFile(std::make_shared<OffsetVfsFile>( | 67 | parent->AddFile(std::make_shared<OffsetVfsFile>( |
| 68 | file, entry.first.size, entry.first.offset + data_offset, entry.second, parent)); | 68 | file, entry.first.size, entry.first.offset + data_offset, entry.second)); |
| 69 | 69 | ||
| 70 | if (entry.first.sibling == ROMFS_ENTRY_EMPTY) | 70 | if (entry.first.sibling == ROMFS_ENTRY_EMPTY) |
| 71 | break; | 71 | break; |
| @@ -79,7 +79,7 @@ void ProcessDirectory(VirtualFile file, size_t dir_offset, size_t file_offset, s | |||
| 79 | while (true) { | 79 | while (true) { |
| 80 | auto entry = GetEntry<DirectoryEntry>(file, dir_offset + this_dir_offset); | 80 | auto entry = GetEntry<DirectoryEntry>(file, dir_offset + this_dir_offset); |
| 81 | auto current = std::make_shared<VectorVfsDirectory>( | 81 | auto current = std::make_shared<VectorVfsDirectory>( |
| 82 | std::vector<VirtualFile>{}, std::vector<VirtualDir>{}, parent, entry.second); | 82 | std::vector<VirtualFile>{}, std::vector<VirtualDir>{}, entry.second); |
| 83 | 83 | ||
| 84 | if (entry.first.child_file != ROMFS_ENTRY_EMPTY) { | 84 | if (entry.first.child_file != ROMFS_ENTRY_EMPTY) { |
| 85 | ProcessFile(file, file_offset, data_offset, entry.first.child_file, current); | 85 | ProcessFile(file, file_offset, data_offset, entry.first.child_file, current); |
| @@ -108,9 +108,9 @@ VirtualDir ExtractRomFS(VirtualFile file) { | |||
| 108 | const u64 file_offset = header.file_meta.offset; | 108 | const u64 file_offset = header.file_meta.offset; |
| 109 | const u64 dir_offset = header.directory_meta.offset + 4; | 109 | const u64 dir_offset = header.directory_meta.offset + 4; |
| 110 | 110 | ||
| 111 | const auto root = | 111 | auto root = |
| 112 | std::make_shared<VectorVfsDirectory>(std::vector<VirtualFile>{}, std::vector<VirtualDir>{}, | 112 | std::make_shared<VectorVfsDirectory>(std::vector<VirtualFile>{}, std::vector<VirtualDir>{}, |
| 113 | file->GetContainingDirectory(), file->GetName()); | 113 | file->GetName(), file->GetContainingDirectory()); |
| 114 | 114 | ||
| 115 | ProcessDirectory(file, dir_offset, file_offset, header.data_offset, 0, root); | 115 | ProcessDirectory(file, dir_offset, file_offset, header.data_offset, 0, root); |
| 116 | 116 | ||
diff --git a/src/core/file_sys/vfs_concat.cpp b/src/core/file_sys/vfs_concat.cpp new file mode 100644 index 000000000..e6bf586a3 --- /dev/null +++ b/src/core/file_sys/vfs_concat.cpp | |||
| @@ -0,0 +1,94 @@ | |||
| 1 | // Copyright 2018 yuzu emulator team | ||
| 2 | // Licensed under GPLv2 or any later version | ||
| 3 | // Refer to the license.txt file included. | ||
| 4 | |||
| 5 | #include <algorithm> | ||
| 6 | #include <utility> | ||
| 7 | |||
| 8 | #include "core/file_sys/vfs_concat.h" | ||
| 9 | |||
| 10 | namespace FileSys { | ||
| 11 | |||
| 12 | VirtualFile ConcatenateFiles(std::vector<VirtualFile> files, std::string name) { | ||
| 13 | if (files.empty()) | ||
| 14 | return nullptr; | ||
| 15 | if (files.size() == 1) | ||
| 16 | return files[0]; | ||
| 17 | |||
| 18 | return std::shared_ptr<VfsFile>(new ConcatenatedVfsFile(std::move(files), std::move(name))); | ||
| 19 | } | ||
| 20 | |||
| 21 | ConcatenatedVfsFile::ConcatenatedVfsFile(std::vector<VirtualFile> files_, std::string name) | ||
| 22 | : name(std::move(name)) { | ||
| 23 | size_t next_offset = 0; | ||
| 24 | for (const auto& file : files_) { | ||
| 25 | files[next_offset] = file; | ||
| 26 | next_offset += file->GetSize(); | ||
| 27 | } | ||
| 28 | } | ||
| 29 | |||
| 30 | std::string ConcatenatedVfsFile::GetName() const { | ||
| 31 | if (files.empty()) | ||
| 32 | return ""; | ||
| 33 | if (!name.empty()) | ||
| 34 | return name; | ||
| 35 | return files.begin()->second->GetName(); | ||
| 36 | } | ||
| 37 | |||
| 38 | size_t ConcatenatedVfsFile::GetSize() const { | ||
| 39 | if (files.empty()) | ||
| 40 | return 0; | ||
| 41 | return files.rbegin()->first + files.rbegin()->second->GetSize(); | ||
| 42 | } | ||
| 43 | |||
| 44 | bool ConcatenatedVfsFile::Resize(size_t new_size) { | ||
| 45 | return false; | ||
| 46 | } | ||
| 47 | |||
| 48 | std::shared_ptr<VfsDirectory> ConcatenatedVfsFile::GetContainingDirectory() const { | ||
| 49 | if (files.empty()) | ||
| 50 | return nullptr; | ||
| 51 | return files.begin()->second->GetContainingDirectory(); | ||
| 52 | } | ||
| 53 | |||
| 54 | bool ConcatenatedVfsFile::IsWritable() const { | ||
| 55 | return false; | ||
| 56 | } | ||
| 57 | |||
| 58 | bool ConcatenatedVfsFile::IsReadable() const { | ||
| 59 | return true; | ||
| 60 | } | ||
| 61 | |||
| 62 | size_t ConcatenatedVfsFile::Read(u8* data, size_t length, size_t offset) const { | ||
| 63 | auto entry = files.end(); | ||
| 64 | for (auto iter = files.begin(); iter != files.end(); ++iter) { | ||
| 65 | if (iter->first > offset) { | ||
| 66 | entry = --iter; | ||
| 67 | break; | ||
| 68 | } | ||
| 69 | } | ||
| 70 | |||
| 71 | // Check if the entry should be the last one. The loop above will make it end(). | ||
| 72 | if (entry == files.end() && offset < files.rbegin()->first + files.rbegin()->second->GetSize()) | ||
| 73 | --entry; | ||
| 74 | |||
| 75 | if (entry == files.end()) | ||
| 76 | return 0; | ||
| 77 | |||
| 78 | const auto remaining = entry->second->GetSize() + offset - entry->first; | ||
| 79 | if (length > remaining) { | ||
| 80 | return entry->second->Read(data, remaining, offset - entry->first) + | ||
| 81 | Read(data + remaining, length - remaining, offset + remaining); | ||
| 82 | } | ||
| 83 | |||
| 84 | return entry->second->Read(data, length, offset - entry->first); | ||
| 85 | } | ||
| 86 | |||
| 87 | size_t ConcatenatedVfsFile::Write(const u8* data, size_t length, size_t offset) { | ||
| 88 | return 0; | ||
| 89 | } | ||
| 90 | |||
| 91 | bool ConcatenatedVfsFile::Rename(std::string_view name) { | ||
| 92 | return false; | ||
| 93 | } | ||
| 94 | } // namespace FileSys | ||
diff --git a/src/core/file_sys/vfs_concat.h b/src/core/file_sys/vfs_concat.h new file mode 100644 index 000000000..686d32515 --- /dev/null +++ b/src/core/file_sys/vfs_concat.h | |||
| @@ -0,0 +1,41 @@ | |||
| 1 | // Copyright 2018 yuzu emulator team | ||
| 2 | // Licensed under GPLv2 or any later version | ||
| 3 | // Refer to the license.txt file included. | ||
| 4 | |||
| 5 | #pragma once | ||
| 6 | |||
| 7 | #include <memory> | ||
| 8 | #include <string_view> | ||
| 9 | #include <boost/container/flat_map.hpp> | ||
| 10 | #include "core/file_sys/vfs.h" | ||
| 11 | |||
| 12 | namespace FileSys { | ||
| 13 | |||
| 14 | // Wrapper function to allow for more efficient handling of files.size() == 0, 1 cases. | ||
| 15 | VirtualFile ConcatenateFiles(std::vector<VirtualFile> files, std::string name = ""); | ||
| 16 | |||
| 17 | // Class that wraps multiple vfs files and concatenates them, making reads seamless. Currently | ||
| 18 | // read-only. | ||
| 19 | class ConcatenatedVfsFile : public VfsFile { | ||
| 20 | friend VirtualFile ConcatenateFiles(std::vector<VirtualFile> files, std::string name); | ||
| 21 | |||
| 22 | ConcatenatedVfsFile(std::vector<VirtualFile> files, std::string name); | ||
| 23 | |||
| 24 | public: | ||
| 25 | std::string GetName() const override; | ||
| 26 | size_t GetSize() const override; | ||
| 27 | bool Resize(size_t new_size) override; | ||
| 28 | std::shared_ptr<VfsDirectory> GetContainingDirectory() const override; | ||
| 29 | bool IsWritable() const override; | ||
| 30 | bool IsReadable() const override; | ||
| 31 | size_t Read(u8* data, size_t length, size_t offset) const override; | ||
| 32 | size_t Write(const u8* data, size_t length, size_t offset) override; | ||
| 33 | bool Rename(std::string_view name) override; | ||
| 34 | |||
| 35 | private: | ||
| 36 | // Maps starting offset to file -- more efficient. | ||
| 37 | boost::container::flat_map<u64, VirtualFile> files; | ||
| 38 | std::string name; | ||
| 39 | }; | ||
| 40 | |||
| 41 | } // namespace FileSys | ||
diff --git a/src/core/file_sys/vfs_real.cpp b/src/core/file_sys/vfs_real.cpp index 1b5919737..0afe515f0 100644 --- a/src/core/file_sys/vfs_real.cpp +++ b/src/core/file_sys/vfs_real.cpp | |||
| @@ -83,8 +83,12 @@ VirtualFile RealVfsFilesystem::OpenFile(std::string_view path_, Mode perms) { | |||
| 83 | 83 | ||
| 84 | VirtualFile RealVfsFilesystem::CreateFile(std::string_view path_, Mode perms) { | 84 | VirtualFile RealVfsFilesystem::CreateFile(std::string_view path_, Mode perms) { |
| 85 | const auto path = FileUtil::SanitizePath(path_, FileUtil::DirectorySeparator::PlatformDefault); | 85 | const auto path = FileUtil::SanitizePath(path_, FileUtil::DirectorySeparator::PlatformDefault); |
| 86 | if (!FileUtil::Exists(path) && !FileUtil::CreateEmptyFile(path)) | 86 | const auto path_fwd = FileUtil::SanitizePath(path, FileUtil::DirectorySeparator::ForwardSlash); |
| 87 | return nullptr; | 87 | if (!FileUtil::Exists(path)) { |
| 88 | FileUtil::CreateFullPath(path_fwd); | ||
| 89 | if (!FileUtil::CreateEmptyFile(path)) | ||
| 90 | return nullptr; | ||
| 91 | } | ||
| 88 | return OpenFile(path, perms); | 92 | return OpenFile(path, perms); |
| 89 | } | 93 | } |
| 90 | 94 | ||
| @@ -140,8 +144,12 @@ VirtualDir RealVfsFilesystem::OpenDirectory(std::string_view path_, Mode perms) | |||
| 140 | 144 | ||
| 141 | VirtualDir RealVfsFilesystem::CreateDirectory(std::string_view path_, Mode perms) { | 145 | VirtualDir RealVfsFilesystem::CreateDirectory(std::string_view path_, Mode perms) { |
| 142 | const auto path = FileUtil::SanitizePath(path_, FileUtil::DirectorySeparator::PlatformDefault); | 146 | const auto path = FileUtil::SanitizePath(path_, FileUtil::DirectorySeparator::PlatformDefault); |
| 143 | if (!FileUtil::Exists(path) && !FileUtil::CreateDir(path)) | 147 | const auto path_fwd = FileUtil::SanitizePath(path, FileUtil::DirectorySeparator::ForwardSlash); |
| 144 | return nullptr; | 148 | if (!FileUtil::Exists(path)) { |
| 149 | FileUtil::CreateFullPath(path_fwd); | ||
| 150 | if (!FileUtil::CreateDir(path)) | ||
| 151 | return nullptr; | ||
| 152 | } | ||
| 145 | // Cannot use make_shared as RealVfsDirectory constructor is private | 153 | // Cannot use make_shared as RealVfsDirectory constructor is private |
| 146 | return std::shared_ptr<RealVfsDirectory>(new RealVfsDirectory(*this, path, perms)); | 154 | return std::shared_ptr<RealVfsDirectory>(new RealVfsDirectory(*this, path, perms)); |
| 147 | } | 155 | } |
| @@ -306,14 +314,14 @@ RealVfsDirectory::RealVfsDirectory(RealVfsFilesystem& base_, const std::string& | |||
| 306 | 314 | ||
| 307 | std::shared_ptr<VfsFile> RealVfsDirectory::GetFileRelative(std::string_view path) const { | 315 | std::shared_ptr<VfsFile> RealVfsDirectory::GetFileRelative(std::string_view path) const { |
| 308 | const auto full_path = FileUtil::SanitizePath(this->path + DIR_SEP + std::string(path)); | 316 | const auto full_path = FileUtil::SanitizePath(this->path + DIR_SEP + std::string(path)); |
| 309 | if (!FileUtil::Exists(full_path)) | 317 | if (!FileUtil::Exists(full_path) || FileUtil::IsDirectory(full_path)) |
| 310 | return nullptr; | 318 | return nullptr; |
| 311 | return base.OpenFile(full_path, perms); | 319 | return base.OpenFile(full_path, perms); |
| 312 | } | 320 | } |
| 313 | 321 | ||
| 314 | std::shared_ptr<VfsDirectory> RealVfsDirectory::GetDirectoryRelative(std::string_view path) const { | 322 | std::shared_ptr<VfsDirectory> RealVfsDirectory::GetDirectoryRelative(std::string_view path) const { |
| 315 | const auto full_path = FileUtil::SanitizePath(this->path + DIR_SEP + std::string(path)); | 323 | const auto full_path = FileUtil::SanitizePath(this->path + DIR_SEP + std::string(path)); |
| 316 | if (!FileUtil::Exists(full_path)) | 324 | if (!FileUtil::Exists(full_path) || !FileUtil::IsDirectory(full_path)) |
| 317 | return nullptr; | 325 | return nullptr; |
| 318 | return base.OpenDirectory(full_path, perms); | 326 | return base.OpenDirectory(full_path, perms); |
| 319 | } | 327 | } |
diff --git a/src/core/file_sys/vfs_real.h b/src/core/file_sys/vfs_real.h index 8a1e79ef6..989803d43 100644 --- a/src/core/file_sys/vfs_real.h +++ b/src/core/file_sys/vfs_real.h | |||
| @@ -5,7 +5,6 @@ | |||
| 5 | #pragma once | 5 | #pragma once |
| 6 | 6 | ||
| 7 | #include <string_view> | 7 | #include <string_view> |
| 8 | |||
| 9 | #include <boost/container/flat_map.hpp> | 8 | #include <boost/container/flat_map.hpp> |
| 10 | #include "common/file_util.h" | 9 | #include "common/file_util.h" |
| 11 | #include "core/file_sys/mode.h" | 10 | #include "core/file_sys/mode.h" |
diff --git a/src/core/file_sys/vfs_vector.cpp b/src/core/file_sys/vfs_vector.cpp index fda603960..98e7c4598 100644 --- a/src/core/file_sys/vfs_vector.cpp +++ b/src/core/file_sys/vfs_vector.cpp | |||
| @@ -8,8 +8,8 @@ | |||
| 8 | 8 | ||
| 9 | namespace FileSys { | 9 | namespace FileSys { |
| 10 | VectorVfsDirectory::VectorVfsDirectory(std::vector<VirtualFile> files_, | 10 | VectorVfsDirectory::VectorVfsDirectory(std::vector<VirtualFile> files_, |
| 11 | std::vector<VirtualDir> dirs_, VirtualDir parent_, | 11 | std::vector<VirtualDir> dirs_, std::string name_, |
| 12 | std::string name_) | 12 | VirtualDir parent_) |
| 13 | : files(std::move(files_)), dirs(std::move(dirs_)), parent(std::move(parent_)), | 13 | : files(std::move(files_)), dirs(std::move(dirs_)), parent(std::move(parent_)), |
| 14 | name(std::move(name_)) {} | 14 | name(std::move(name_)) {} |
| 15 | 15 | ||
diff --git a/src/core/file_sys/vfs_vector.h b/src/core/file_sys/vfs_vector.h index b3b468233..179f62e4b 100644 --- a/src/core/file_sys/vfs_vector.h +++ b/src/core/file_sys/vfs_vector.h | |||
| @@ -13,8 +13,8 @@ namespace FileSys { | |||
| 13 | class VectorVfsDirectory : public VfsDirectory { | 13 | class VectorVfsDirectory : public VfsDirectory { |
| 14 | public: | 14 | public: |
| 15 | explicit VectorVfsDirectory(std::vector<VirtualFile> files = {}, | 15 | explicit VectorVfsDirectory(std::vector<VirtualFile> files = {}, |
| 16 | std::vector<VirtualDir> dirs = {}, VirtualDir parent = nullptr, | 16 | std::vector<VirtualDir> dirs = {}, std::string name = "", |
| 17 | std::string name = ""); | 17 | VirtualDir parent = nullptr); |
| 18 | 18 | ||
| 19 | std::vector<std::shared_ptr<VfsFile>> GetFiles() const override; | 19 | std::vector<std::shared_ptr<VfsFile>> GetFiles() const override; |
| 20 | std::vector<std::shared_ptr<VfsDirectory>> GetSubdirectories() const override; | 20 | std::vector<std::shared_ptr<VfsDirectory>> GetSubdirectories() const override; |
diff --git a/src/core/hle/service/filesystem/filesystem.cpp b/src/core/hle/service/filesystem/filesystem.cpp index 5e416cde2..da658cbe6 100644 --- a/src/core/hle/service/filesystem/filesystem.cpp +++ b/src/core/hle/service/filesystem/filesystem.cpp | |||
| @@ -226,6 +226,7 @@ ResultVal<FileSys::EntryType> VfsDirectoryServiceWrapper::GetEntryType( | |||
| 226 | static std::unique_ptr<FileSys::RomFSFactory> romfs_factory; | 226 | static std::unique_ptr<FileSys::RomFSFactory> romfs_factory; |
| 227 | static std::unique_ptr<FileSys::SaveDataFactory> save_data_factory; | 227 | static std::unique_ptr<FileSys::SaveDataFactory> save_data_factory; |
| 228 | static std::unique_ptr<FileSys::SDMCFactory> sdmc_factory; | 228 | static std::unique_ptr<FileSys::SDMCFactory> sdmc_factory; |
| 229 | static std::unique_ptr<FileSys::BISFactory> bis_factory; | ||
| 229 | 230 | ||
| 230 | ResultCode RegisterRomFS(std::unique_ptr<FileSys::RomFSFactory>&& factory) { | 231 | ResultCode RegisterRomFS(std::unique_ptr<FileSys::RomFSFactory>&& factory) { |
| 231 | ASSERT_MSG(romfs_factory == nullptr, "Tried to register a second RomFS"); | 232 | ASSERT_MSG(romfs_factory == nullptr, "Tried to register a second RomFS"); |
| @@ -248,6 +249,13 @@ ResultCode RegisterSDMC(std::unique_ptr<FileSys::SDMCFactory>&& factory) { | |||
| 248 | return RESULT_SUCCESS; | 249 | return RESULT_SUCCESS; |
| 249 | } | 250 | } |
| 250 | 251 | ||
| 252 | ResultCode RegisterBIS(std::unique_ptr<FileSys::BISFactory>&& factory) { | ||
| 253 | ASSERT_MSG(bis_factory == nullptr, "Tried to register a second BIS"); | ||
| 254 | bis_factory = std::move(factory); | ||
| 255 | LOG_DEBUG(Service_FS, "Registred BIS"); | ||
| 256 | return RESULT_SUCCESS; | ||
| 257 | } | ||
| 258 | |||
| 251 | ResultVal<FileSys::VirtualFile> OpenRomFS(u64 title_id) { | 259 | ResultVal<FileSys::VirtualFile> OpenRomFS(u64 title_id) { |
| 252 | LOG_TRACE(Service_FS, "Opening RomFS for title_id={:016X}", title_id); | 260 | LOG_TRACE(Service_FS, "Opening RomFS for title_id={:016X}", title_id); |
| 253 | 261 | ||
| @@ -281,6 +289,14 @@ ResultVal<FileSys::VirtualDir> OpenSDMC() { | |||
| 281 | return sdmc_factory->Open(); | 289 | return sdmc_factory->Open(); |
| 282 | } | 290 | } |
| 283 | 291 | ||
| 292 | std::shared_ptr<FileSys::RegisteredCache> GetSystemNANDContents() { | ||
| 293 | return bis_factory->GetSystemNANDContents(); | ||
| 294 | } | ||
| 295 | |||
| 296 | std::shared_ptr<FileSys::RegisteredCache> GetUserNANDContents() { | ||
| 297 | return bis_factory->GetUserNANDContents(); | ||
| 298 | } | ||
| 299 | |||
| 284 | void RegisterFileSystems(const FileSys::VirtualFilesystem& vfs) { | 300 | void RegisterFileSystems(const FileSys::VirtualFilesystem& vfs) { |
| 285 | romfs_factory = nullptr; | 301 | romfs_factory = nullptr; |
| 286 | save_data_factory = nullptr; | 302 | save_data_factory = nullptr; |
| @@ -291,6 +307,9 @@ void RegisterFileSystems(const FileSys::VirtualFilesystem& vfs) { | |||
| 291 | auto sd_directory = vfs->OpenDirectory(FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir), | 307 | auto sd_directory = vfs->OpenDirectory(FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir), |
| 292 | FileSys::Mode::ReadWrite); | 308 | FileSys::Mode::ReadWrite); |
| 293 | 309 | ||
| 310 | if (bis_factory == nullptr) | ||
| 311 | bis_factory = std::make_unique<FileSys::BISFactory>(nand_directory); | ||
| 312 | |||
| 294 | auto savedata = std::make_unique<FileSys::SaveDataFactory>(std::move(nand_directory)); | 313 | auto savedata = std::make_unique<FileSys::SaveDataFactory>(std::move(nand_directory)); |
| 295 | save_data_factory = std::move(savedata); | 314 | save_data_factory = std::move(savedata); |
| 296 | 315 | ||
diff --git a/src/core/hle/service/filesystem/filesystem.h b/src/core/hle/service/filesystem/filesystem.h index 462c13f20..1d6f922dd 100644 --- a/src/core/hle/service/filesystem/filesystem.h +++ b/src/core/hle/service/filesystem/filesystem.h | |||
| @@ -6,6 +6,7 @@ | |||
| 6 | 6 | ||
| 7 | #include <memory> | 7 | #include <memory> |
| 8 | #include "common/common_types.h" | 8 | #include "common/common_types.h" |
| 9 | #include "core/file_sys/bis_factory.h" | ||
| 9 | #include "core/file_sys/directory.h" | 10 | #include "core/file_sys/directory.h" |
| 10 | #include "core/file_sys/mode.h" | 11 | #include "core/file_sys/mode.h" |
| 11 | #include "core/file_sys/romfs_factory.h" | 12 | #include "core/file_sys/romfs_factory.h" |
| @@ -24,16 +25,15 @@ namespace FileSystem { | |||
| 24 | ResultCode RegisterRomFS(std::unique_ptr<FileSys::RomFSFactory>&& factory); | 25 | ResultCode RegisterRomFS(std::unique_ptr<FileSys::RomFSFactory>&& factory); |
| 25 | ResultCode RegisterSaveData(std::unique_ptr<FileSys::SaveDataFactory>&& factory); | 26 | ResultCode RegisterSaveData(std::unique_ptr<FileSys::SaveDataFactory>&& factory); |
| 26 | ResultCode RegisterSDMC(std::unique_ptr<FileSys::SDMCFactory>&& factory); | 27 | ResultCode RegisterSDMC(std::unique_ptr<FileSys::SDMCFactory>&& factory); |
| 28 | ResultCode RegisterBIS(std::unique_ptr<FileSys::BISFactory>&& factory); | ||
| 27 | 29 | ||
| 28 | // TODO(DarkLordZach): BIS Filesystem | ||
| 29 | // ResultCode RegisterBIS(std::unique_ptr<FileSys::BISFactory>&& factory); | ||
| 30 | ResultVal<FileSys::VirtualFile> OpenRomFS(u64 title_id); | 30 | ResultVal<FileSys::VirtualFile> OpenRomFS(u64 title_id); |
| 31 | ResultVal<FileSys::VirtualDir> OpenSaveData(FileSys::SaveDataSpaceId space, | 31 | ResultVal<FileSys::VirtualDir> OpenSaveData(FileSys::SaveDataSpaceId space, |
| 32 | FileSys::SaveDataDescriptor save_struct); | 32 | FileSys::SaveDataDescriptor save_struct); |
| 33 | ResultVal<FileSys::VirtualDir> OpenSDMC(); | 33 | ResultVal<FileSys::VirtualDir> OpenSDMC(); |
| 34 | 34 | ||
| 35 | // TODO(DarkLordZach): BIS Filesystem | 35 | std::shared_ptr<FileSys::RegisteredCache> GetSystemNANDContents(); |
| 36 | // ResultVal<std::unique_ptr<FileSys::FileSystemBackend>> OpenBIS(); | 36 | std::shared_ptr<FileSys::RegisteredCache> GetUserNANDContents(); |
| 37 | 37 | ||
| 38 | /// Registers all Filesystem services with the specified service manager. | 38 | /// Registers all Filesystem services with the specified service manager. |
| 39 | void InstallInterfaces(SM::ServiceManager& service_manager, const FileSys::VirtualFilesystem& vfs); | 39 | void InstallInterfaces(SM::ServiceManager& service_manager, const FileSys::VirtualFilesystem& vfs); |
diff --git a/src/core/loader/loader.cpp b/src/core/loader/loader.cpp index 5e07a3f10..70ef5d240 100644 --- a/src/core/loader/loader.cpp +++ b/src/core/loader/loader.cpp | |||
| @@ -41,6 +41,8 @@ FileType IdentifyFile(FileSys::VirtualFile file) { | |||
| 41 | FileType GuessFromFilename(const std::string& name) { | 41 | FileType GuessFromFilename(const std::string& name) { |
| 42 | if (name == "main") | 42 | if (name == "main") |
| 43 | return FileType::DeconstructedRomDirectory; | 43 | return FileType::DeconstructedRomDirectory; |
| 44 | if (name == "00") | ||
| 45 | return FileType::NCA; | ||
| 44 | 46 | ||
| 45 | const std::string extension = | 47 | const std::string extension = |
| 46 | Common::ToLower(std::string(FileUtil::GetExtensionFromFilename(name))); | 48 | Common::ToLower(std::string(FileUtil::GetExtensionFromFilename(name))); |
diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp index 85cb12594..f867118d9 100644 --- a/src/yuzu/game_list.cpp +++ b/src/yuzu/game_list.cpp | |||
| @@ -2,6 +2,7 @@ | |||
| 2 | // Licensed under GPLv2 or any later version | 2 | // Licensed under GPLv2 or any later version |
| 3 | // Refer to the license.txt file included. | 3 | // Refer to the license.txt file included. |
| 4 | 4 | ||
| 5 | #include <regex> | ||
| 5 | #include <QApplication> | 6 | #include <QApplication> |
| 6 | #include <QDir> | 7 | #include <QDir> |
| 7 | #include <QFileInfo> | 8 | #include <QFileInfo> |
| @@ -402,12 +403,72 @@ void GameList::RefreshGameDirectory() { | |||
| 402 | } | 403 | } |
| 403 | } | 404 | } |
| 404 | 405 | ||
| 405 | void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion) { | 406 | static void GetMetadataFromControlNCA(const std::shared_ptr<FileSys::NCA>& nca, |
| 406 | boost::container::flat_map<u64, std::shared_ptr<FileSys::NCA>> nca_control_map; | 407 | std::vector<u8>& icon, std::string& name) { |
| 408 | const auto control_dir = FileSys::ExtractRomFS(nca->GetRomFS()); | ||
| 409 | if (control_dir == nullptr) | ||
| 410 | return; | ||
| 411 | |||
| 412 | const auto nacp_file = control_dir->GetFile("control.nacp"); | ||
| 413 | if (nacp_file == nullptr) | ||
| 414 | return; | ||
| 415 | FileSys::NACP nacp(nacp_file); | ||
| 416 | name = nacp.GetApplicationName(); | ||
| 417 | |||
| 418 | FileSys::VirtualFile icon_file = nullptr; | ||
| 419 | for (const auto& language : FileSys::LANGUAGE_NAMES) { | ||
| 420 | icon_file = control_dir->GetFile("icon_" + std::string(language) + ".dat"); | ||
| 421 | if (icon_file != nullptr) { | ||
| 422 | icon = icon_file->ReadAllBytes(); | ||
| 423 | break; | ||
| 424 | } | ||
| 425 | } | ||
| 426 | } | ||
| 427 | |||
| 428 | void GameListWorker::AddInstalledTitlesToGameList() { | ||
| 429 | const auto usernand = Service::FileSystem::GetUserNANDContents(); | ||
| 430 | const auto installed_games = usernand->ListEntriesFilter(FileSys::TitleType::Application, | ||
| 431 | FileSys::ContentRecordType::Program); | ||
| 432 | |||
| 433 | for (const auto& game : installed_games) { | ||
| 434 | const auto& file = usernand->GetEntryRaw(game); | ||
| 435 | std::unique_ptr<Loader::AppLoader> loader = Loader::GetLoader(file); | ||
| 436 | if (!loader) | ||
| 437 | continue; | ||
| 438 | |||
| 439 | std::vector<u8> icon; | ||
| 440 | std::string name; | ||
| 441 | u64 program_id; | ||
| 442 | loader->ReadProgramId(program_id); | ||
| 443 | |||
| 444 | const auto& control = | ||
| 445 | usernand->GetEntry(game.title_id, FileSys::ContentRecordType::Control); | ||
| 446 | if (control != nullptr) | ||
| 447 | GetMetadataFromControlNCA(control, icon, name); | ||
| 448 | emit EntryReady({ | ||
| 449 | new GameListItemPath( | ||
| 450 | FormatGameName(file->GetFullPath()), icon, QString::fromStdString(name), | ||
| 451 | QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType())), | ||
| 452 | program_id), | ||
| 453 | new GameListItem( | ||
| 454 | QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))), | ||
| 455 | new GameListItemSize(file->GetSize()), | ||
| 456 | }); | ||
| 457 | } | ||
| 458 | |||
| 459 | const auto control_data = usernand->ListEntriesFilter(FileSys::TitleType::Application, | ||
| 460 | FileSys::ContentRecordType::Control); | ||
| 461 | |||
| 462 | for (const auto& entry : control_data) { | ||
| 463 | const auto nca = usernand->GetEntry(entry); | ||
| 464 | if (nca != nullptr) | ||
| 465 | nca_control_map.insert_or_assign(entry.title_id, nca); | ||
| 466 | } | ||
| 467 | } | ||
| 407 | 468 | ||
| 408 | const auto nca_control_callback = | 469 | void GameListWorker::FillControlMap(const std::string& dir_path) { |
| 409 | [this, &nca_control_map](u64* num_entries_out, const std::string& directory, | 470 | const auto nca_control_callback = [this](u64* num_entries_out, const std::string& directory, |
| 410 | const std::string& virtual_name) -> bool { | 471 | const std::string& virtual_name) -> bool { |
| 411 | std::string physical_name = directory + DIR_SEP + virtual_name; | 472 | std::string physical_name = directory + DIR_SEP + virtual_name; |
| 412 | 473 | ||
| 413 | if (stop_processing) | 474 | if (stop_processing) |
| @@ -425,10 +486,11 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign | |||
| 425 | }; | 486 | }; |
| 426 | 487 | ||
| 427 | FileUtil::ForeachDirectoryEntry(nullptr, dir_path, nca_control_callback); | 488 | FileUtil::ForeachDirectoryEntry(nullptr, dir_path, nca_control_callback); |
| 489 | } | ||
| 428 | 490 | ||
| 429 | const auto callback = [this, recursion, | 491 | void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion) { |
| 430 | &nca_control_map](u64* num_entries_out, const std::string& directory, | 492 | const auto callback = [this, recursion](u64* num_entries_out, const std::string& directory, |
| 431 | const std::string& virtual_name) -> bool { | 493 | const std::string& virtual_name) -> bool { |
| 432 | std::string physical_name = directory + DIR_SEP + virtual_name; | 494 | std::string physical_name = directory + DIR_SEP + virtual_name; |
| 433 | 495 | ||
| 434 | if (stop_processing) | 496 | if (stop_processing) |
| @@ -458,20 +520,7 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign | |||
| 458 | // Use from metadata pool. | 520 | // Use from metadata pool. |
| 459 | if (nca_control_map.find(program_id) != nca_control_map.end()) { | 521 | if (nca_control_map.find(program_id) != nca_control_map.end()) { |
| 460 | const auto nca = nca_control_map[program_id]; | 522 | const auto nca = nca_control_map[program_id]; |
| 461 | const auto control_dir = FileSys::ExtractRomFS(nca->GetRomFS()); | 523 | GetMetadataFromControlNCA(nca, icon, name); |
| 462 | |||
| 463 | const auto nacp_file = control_dir->GetFile("control.nacp"); | ||
| 464 | FileSys::NACP nacp(nacp_file); | ||
| 465 | name = nacp.GetApplicationName(); | ||
| 466 | |||
| 467 | FileSys::VirtualFile icon_file = nullptr; | ||
| 468 | for (const auto& language : FileSys::LANGUAGE_NAMES) { | ||
| 469 | icon_file = control_dir->GetFile("icon_" + std::string(language) + ".dat"); | ||
| 470 | if (icon_file != nullptr) { | ||
| 471 | icon = icon_file->ReadAllBytes(); | ||
| 472 | break; | ||
| 473 | } | ||
| 474 | } | ||
| 475 | } | 524 | } |
| 476 | } | 525 | } |
| 477 | 526 | ||
| @@ -498,7 +547,10 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign | |||
| 498 | void GameListWorker::run() { | 547 | void GameListWorker::run() { |
| 499 | stop_processing = false; | 548 | stop_processing = false; |
| 500 | watch_list.append(dir_path); | 549 | watch_list.append(dir_path); |
| 550 | FillControlMap(dir_path.toStdString()); | ||
| 551 | AddInstalledTitlesToGameList(); | ||
| 501 | AddFstEntriesToGameList(dir_path.toStdString(), deep_scan ? 256 : 0); | 552 | AddFstEntriesToGameList(dir_path.toStdString(), deep_scan ? 256 : 0); |
| 553 | nca_control_map.clear(); | ||
| 502 | emit Finished(watch_list); | 554 | emit Finished(watch_list); |
| 503 | } | 555 | } |
| 504 | 556 | ||
diff --git a/src/yuzu/game_list_p.h b/src/yuzu/game_list_p.h index 8fe5e8b80..10c2ef075 100644 --- a/src/yuzu/game_list_p.h +++ b/src/yuzu/game_list_p.h | |||
| @@ -163,10 +163,13 @@ signals: | |||
| 163 | 163 | ||
| 164 | private: | 164 | private: |
| 165 | FileSys::VirtualFilesystem vfs; | 165 | FileSys::VirtualFilesystem vfs; |
| 166 | std::map<u64, std::shared_ptr<FileSys::NCA>> nca_control_map; | ||
| 166 | QStringList watch_list; | 167 | QStringList watch_list; |
| 167 | QString dir_path; | 168 | QString dir_path; |
| 168 | bool deep_scan; | 169 | bool deep_scan; |
| 169 | std::atomic_bool stop_processing; | 170 | std::atomic_bool stop_processing; |
| 170 | 171 | ||
| 172 | void AddInstalledTitlesToGameList(); | ||
| 173 | void FillControlMap(const std::string& dir_path); | ||
| 171 | void AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion = 0); | 174 | void AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion = 0); |
| 172 | }; | 175 | }; |
diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 4bbea3f3c..f7eee7769 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp | |||
| @@ -27,6 +27,7 @@ | |||
| 27 | #include "common/string_util.h" | 27 | #include "common/string_util.h" |
| 28 | #include "core/core.h" | 28 | #include "core/core.h" |
| 29 | #include "core/crypto/key_manager.h" | 29 | #include "core/crypto/key_manager.h" |
| 30 | #include "core/file_sys/card_image.h" | ||
| 30 | #include "core/file_sys/vfs_real.h" | 31 | #include "core/file_sys/vfs_real.h" |
| 31 | #include "core/gdbstub/gdbstub.h" | 32 | #include "core/gdbstub/gdbstub.h" |
| 32 | #include "core/loader/loader.h" | 33 | #include "core/loader/loader.h" |
| @@ -117,6 +118,9 @@ GMainWindow::GMainWindow() | |||
| 117 | .arg(Common::g_build_name, Common::g_scm_branch, Common::g_scm_desc)); | 118 | .arg(Common::g_build_name, Common::g_scm_branch, Common::g_scm_desc)); |
| 118 | show(); | 119 | show(); |
| 119 | 120 | ||
| 121 | // Necessary to load titles from nand in gamelist. | ||
| 122 | Service::FileSystem::RegisterBIS(std::make_unique<FileSys::BISFactory>(vfs->OpenDirectory( | ||
| 123 | FileUtil::GetUserPath(FileUtil::UserPath::NANDDir), FileSys::Mode::ReadWrite))); | ||
| 120 | game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan); | 124 | game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan); |
| 121 | 125 | ||
| 122 | // Show one-time "callout" messages to the user | 126 | // Show one-time "callout" messages to the user |
| @@ -312,6 +316,8 @@ void GMainWindow::ConnectMenuEvents() { | |||
| 312 | // File | 316 | // File |
| 313 | connect(ui.action_Load_File, &QAction::triggered, this, &GMainWindow::OnMenuLoadFile); | 317 | connect(ui.action_Load_File, &QAction::triggered, this, &GMainWindow::OnMenuLoadFile); |
| 314 | connect(ui.action_Load_Folder, &QAction::triggered, this, &GMainWindow::OnMenuLoadFolder); | 318 | connect(ui.action_Load_Folder, &QAction::triggered, this, &GMainWindow::OnMenuLoadFolder); |
| 319 | connect(ui.action_Install_File_NAND, &QAction::triggered, this, | ||
| 320 | &GMainWindow::OnMenuInstallToNAND); | ||
| 315 | connect(ui.action_Select_Game_List_Root, &QAction::triggered, this, | 321 | connect(ui.action_Select_Game_List_Root, &QAction::triggered, this, |
| 316 | &GMainWindow::OnMenuSelectGameListRoot); | 322 | &GMainWindow::OnMenuSelectGameListRoot); |
| 317 | connect(ui.action_Exit, &QAction::triggered, this, &QMainWindow::close); | 323 | connect(ui.action_Exit, &QAction::triggered, this, &QMainWindow::close); |
| @@ -615,6 +621,143 @@ void GMainWindow::OnMenuLoadFolder() { | |||
| 615 | } | 621 | } |
| 616 | } | 622 | } |
| 617 | 623 | ||
| 624 | void GMainWindow::OnMenuInstallToNAND() { | ||
| 625 | const QString file_filter = | ||
| 626 | tr("Installable Switch File (*.nca *.xci);;Nintendo Content Archive (*.nca);;NX Cartridge " | ||
| 627 | "Image (*.xci)"); | ||
| 628 | QString filename = QFileDialog::getOpenFileName(this, tr("Install File"), | ||
| 629 | UISettings::values.roms_path, file_filter); | ||
| 630 | |||
| 631 | const auto qt_raw_copy = [this](FileSys::VirtualFile src, FileSys::VirtualFile dest) { | ||
| 632 | if (src == nullptr || dest == nullptr) | ||
| 633 | return false; | ||
| 634 | if (!dest->Resize(src->GetSize())) | ||
| 635 | return false; | ||
| 636 | |||
| 637 | QProgressDialog progress(fmt::format("Installing file \"{}\"...", src->GetName()).c_str(), | ||
| 638 | "Cancel", 0, src->GetSize() / 0x1000, this); | ||
| 639 | progress.setWindowModality(Qt::WindowModal); | ||
| 640 | |||
| 641 | std::array<u8, 0x1000> buffer{}; | ||
| 642 | for (size_t i = 0; i < src->GetSize(); i += 0x1000) { | ||
| 643 | if (progress.wasCanceled()) { | ||
| 644 | dest->Resize(0); | ||
| 645 | return false; | ||
| 646 | } | ||
| 647 | |||
| 648 | progress.setValue(i / 0x1000); | ||
| 649 | const auto read = src->Read(buffer.data(), buffer.size(), i); | ||
| 650 | dest->Write(buffer.data(), read, i); | ||
| 651 | } | ||
| 652 | |||
| 653 | return true; | ||
| 654 | }; | ||
| 655 | |||
| 656 | const auto success = [this]() { | ||
| 657 | QMessageBox::information(this, tr("Successfully Installed"), | ||
| 658 | tr("The file was successfully installed.")); | ||
| 659 | game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan); | ||
| 660 | }; | ||
| 661 | |||
| 662 | const auto failed = [this]() { | ||
| 663 | QMessageBox::warning( | ||
| 664 | this, tr("Failed to Install"), | ||
| 665 | tr("There was an error while attempting to install the provided file. It " | ||
| 666 | "could have an incorrect format or be missing metadata. Please " | ||
| 667 | "double-check your file and try again.")); | ||
| 668 | }; | ||
| 669 | |||
| 670 | const auto overwrite = [this]() { | ||
| 671 | return QMessageBox::question(this, "Failed to Install", | ||
| 672 | "The file you are attempting to install already exists " | ||
| 673 | "in the cache. Would you like to overwrite it?") == | ||
| 674 | QMessageBox::Yes; | ||
| 675 | }; | ||
| 676 | |||
| 677 | if (!filename.isEmpty()) { | ||
| 678 | if (filename.endsWith("xci", Qt::CaseInsensitive)) { | ||
| 679 | const auto xci = std::make_shared<FileSys::XCI>( | ||
| 680 | vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read)); | ||
| 681 | if (xci->GetStatus() != Loader::ResultStatus::Success) { | ||
| 682 | failed(); | ||
| 683 | return; | ||
| 684 | } | ||
| 685 | const auto res = | ||
| 686 | Service::FileSystem::GetUserNANDContents()->InstallEntry(xci, false, qt_raw_copy); | ||
| 687 | if (res == FileSys::InstallResult::Success) { | ||
| 688 | success(); | ||
| 689 | } else { | ||
| 690 | if (res == FileSys::InstallResult::ErrorAlreadyExists) { | ||
| 691 | if (overwrite()) { | ||
| 692 | const auto res2 = Service::FileSystem::GetUserNANDContents()->InstallEntry( | ||
| 693 | xci, true, qt_raw_copy); | ||
| 694 | if (res2 == FileSys::InstallResult::Success) { | ||
| 695 | success(); | ||
| 696 | } else { | ||
| 697 | failed(); | ||
| 698 | } | ||
| 699 | } | ||
| 700 | } else { | ||
| 701 | failed(); | ||
| 702 | } | ||
| 703 | } | ||
| 704 | } else { | ||
| 705 | const auto nca = std::make_shared<FileSys::NCA>( | ||
| 706 | vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read)); | ||
| 707 | if (nca->GetStatus() != Loader::ResultStatus::Success) { | ||
| 708 | failed(); | ||
| 709 | return; | ||
| 710 | } | ||
| 711 | |||
| 712 | static const QStringList tt_options{"System Application", | ||
| 713 | "System Archive", | ||
| 714 | "System Application Update", | ||
| 715 | "Firmware Package (Type A)", | ||
| 716 | "Firmware Package (Type B)", | ||
| 717 | "Game", | ||
| 718 | "Game Update", | ||
| 719 | "Game DLC", | ||
| 720 | "Delta Title"}; | ||
| 721 | bool ok; | ||
| 722 | const auto item = QInputDialog::getItem( | ||
| 723 | this, tr("Select NCA Install Type..."), | ||
| 724 | tr("Please select the type of title you would like to install this NCA as:\n(In " | ||
| 725 | "most instances, the default 'Game' is fine.)"), | ||
| 726 | tt_options, 5, false, &ok); | ||
| 727 | |||
| 728 | auto index = tt_options.indexOf(item); | ||
| 729 | if (!ok || index == -1) { | ||
| 730 | QMessageBox::warning(this, tr("Failed to Install"), | ||
| 731 | tr("The title type you selected for the NCA is invalid.")); | ||
| 732 | return; | ||
| 733 | } | ||
| 734 | |||
| 735 | if (index >= 5) | ||
| 736 | index += 0x7B; | ||
| 737 | |||
| 738 | const auto res = Service::FileSystem::GetUserNANDContents()->InstallEntry( | ||
| 739 | nca, static_cast<FileSys::TitleType>(index), false, qt_raw_copy); | ||
| 740 | if (res == FileSys::InstallResult::Success) { | ||
| 741 | success(); | ||
| 742 | } else { | ||
| 743 | if (res == FileSys::InstallResult::ErrorAlreadyExists) { | ||
| 744 | if (overwrite()) { | ||
| 745 | const auto res2 = Service::FileSystem::GetUserNANDContents()->InstallEntry( | ||
| 746 | nca, static_cast<FileSys::TitleType>(index), true, qt_raw_copy); | ||
| 747 | if (res2 == FileSys::InstallResult::Success) { | ||
| 748 | success(); | ||
| 749 | } else { | ||
| 750 | failed(); | ||
| 751 | } | ||
| 752 | } | ||
| 753 | } else { | ||
| 754 | failed(); | ||
| 755 | } | ||
| 756 | } | ||
| 757 | } | ||
| 758 | } | ||
| 759 | } | ||
| 760 | |||
| 618 | void GMainWindow::OnMenuSelectGameListRoot() { | 761 | void GMainWindow::OnMenuSelectGameListRoot() { |
| 619 | QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory")); | 762 | QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory")); |
| 620 | if (!dir_path.isEmpty()) { | 763 | if (!dir_path.isEmpty()) { |
diff --git a/src/yuzu/main.h b/src/yuzu/main.h index 74487c58c..5f4d2ab9a 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h | |||
| @@ -125,6 +125,7 @@ private slots: | |||
| 125 | void OnGameListOpenSaveFolder(u64 program_id); | 125 | void OnGameListOpenSaveFolder(u64 program_id); |
| 126 | void OnMenuLoadFile(); | 126 | void OnMenuLoadFile(); |
| 127 | void OnMenuLoadFolder(); | 127 | void OnMenuLoadFolder(); |
| 128 | void OnMenuInstallToNAND(); | ||
| 128 | /// Called whenever a user selects the "File->Select Game List Root" menu item | 129 | /// Called whenever a user selects the "File->Select Game List Root" menu item |
| 129 | void OnMenuSelectGameListRoot(); | 130 | void OnMenuSelectGameListRoot(); |
| 130 | void OnMenuRecentFile(); | 131 | void OnMenuRecentFile(); |
diff --git a/src/yuzu/main.ui b/src/yuzu/main.ui index 22c4cad08..a3bfb2af3 100644 --- a/src/yuzu/main.ui +++ b/src/yuzu/main.ui | |||
| @@ -57,6 +57,8 @@ | |||
| 57 | <string>Recent Files</string> | 57 | <string>Recent Files</string> |
| 58 | </property> | 58 | </property> |
| 59 | </widget> | 59 | </widget> |
| 60 | <addaction name="action_Install_File_NAND" /> | ||
| 61 | <addaction name="separator"/> | ||
| 60 | <addaction name="action_Load_File"/> | 62 | <addaction name="action_Load_File"/> |
| 61 | <addaction name="action_Load_Folder"/> | 63 | <addaction name="action_Load_Folder"/> |
| 62 | <addaction name="separator"/> | 64 | <addaction name="separator"/> |
| @@ -102,6 +104,11 @@ | |||
| 102 | <addaction name="menu_View"/> | 104 | <addaction name="menu_View"/> |
| 103 | <addaction name="menu_Help"/> | 105 | <addaction name="menu_Help"/> |
| 104 | </widget> | 106 | </widget> |
| 107 | <action name="action_Install_File_NAND"> | ||
| 108 | <property name="text"> | ||
| 109 | <string>Install File to NAND...</string> | ||
| 110 | </property> | ||
| 111 | </action> | ||
| 105 | <action name="action_Load_File"> | 112 | <action name="action_Load_File"> |
| 106 | <property name="text"> | 113 | <property name="text"> |
| 107 | <string>Load File...</string> | 114 | <string>Load File...</string> |