mirror of
				https://git.zaroz.cloud/nintendo-back-up/yuzu/yuzu-mainline.git
				synced 2025-03-21 01:53:15 +00:00 
			
		
		
		
	Merge pull request #1005 from DarkLordZach/registered-fmt
file_sys: Add support for registration format
This commit is contained in:
		
						commit
						c594ec3417
					
				| @ -38,6 +38,8 @@ add_library(common STATIC | ||||
|     file_util.cpp | ||||
|     file_util.h | ||||
|     hash.h | ||||
|     hex_util.cpp | ||||
|     hex_util.h | ||||
|     logging/backend.cpp | ||||
|     logging/backend.h | ||||
|     logging/filter.cpp | ||||
|  | ||||
| @ -750,6 +750,12 @@ std::string GetHactoolConfigurationPath() { | ||||
| #endif | ||||
| } | ||||
| 
 | ||||
| std::string GetNANDRegistrationDir(bool system) { | ||||
|     if (system) | ||||
|         return GetUserPath(UserPath::NANDDir) + "system/Contents/registered/"; | ||||
|     return GetUserPath(UserPath::NANDDir) + "user/Contents/registered/"; | ||||
| } | ||||
| 
 | ||||
| size_t WriteStringToFile(bool text_file, const std::string& str, const char* filename) { | ||||
|     return FileUtil::IOFile(filename, text_file ? "w" : "wb").WriteBytes(str.data(), str.size()); | ||||
| } | ||||
|  | ||||
| @ -129,6 +129,8 @@ const std::string& GetUserPath(UserPath path, const std::string& new_path = ""); | ||||
| 
 | ||||
| std::string GetHactoolConfigurationPath(); | ||||
| 
 | ||||
| std::string GetNANDRegistrationDir(bool system = false); | ||||
| 
 | ||||
| // Returns the path to where the sys file are
 | ||||
| std::string GetSysDirectory(); | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										27
									
								
								src/common/hex_util.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/common/hex_util.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,27 @@ | ||||
| // Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project
 | ||||
| // Licensed under GPLv2 or any later version
 | ||||
| // Refer to the license.txt file included.
 | ||||
| 
 | ||||
| #include "common/hex_util.h" | ||||
| 
 | ||||
| u8 ToHexNibble(char c1) { | ||||
|     if (c1 >= 65 && c1 <= 70) | ||||
|         return c1 - 55; | ||||
|     if (c1 >= 97 && c1 <= 102) | ||||
|         return c1 - 87; | ||||
|     if (c1 >= 48 && c1 <= 57) | ||||
|         return c1 - 48; | ||||
|     throw std::logic_error("Invalid hex digit"); | ||||
| } | ||||
| 
 | ||||
| std::array<u8, 16> operator""_array16(const char* str, size_t len) { | ||||
|     if (len != 32) | ||||
|         throw std::logic_error("Not of correct size."); | ||||
|     return HexStringToArray<16>(str); | ||||
| } | ||||
| 
 | ||||
| std::array<u8, 32> operator""_array32(const char* str, size_t len) { | ||||
|     if (len != 64) | ||||
|         throw std::logic_error("Not of correct size."); | ||||
|     return HexStringToArray<32>(str); | ||||
| } | ||||
							
								
								
									
										37
									
								
								src/common/hex_util.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/common/hex_util.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,37 @@ | ||||
| // Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project
 | ||||
| // Licensed under GPLv2 or any later version
 | ||||
| // Refer to the license.txt file included.
 | ||||
| 
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include <array> | ||||
| #include <cstddef> | ||||
| #include <string> | ||||
| #include <fmt/format.h> | ||||
| #include "common/common_types.h" | ||||
| 
 | ||||
| u8 ToHexNibble(char c1); | ||||
| 
 | ||||
| template <size_t Size, bool le = false> | ||||
| std::array<u8, Size> HexStringToArray(std::string_view str) { | ||||
|     std::array<u8, Size> out{}; | ||||
|     if constexpr (le) { | ||||
|         for (size_t i = 2 * Size - 2; i <= 2 * Size; i -= 2) | ||||
|             out[i / 2] = (ToHexNibble(str[i]) << 4) | ToHexNibble(str[i + 1]); | ||||
|     } else { | ||||
|         for (size_t i = 0; i < 2 * Size; i += 2) | ||||
|             out[i / 2] = (ToHexNibble(str[i]) << 4) | ToHexNibble(str[i + 1]); | ||||
|     } | ||||
|     return out; | ||||
| } | ||||
| 
 | ||||
| template <size_t Size> | ||||
| std::string HexArrayToString(std::array<u8, Size> array, bool upper = true) { | ||||
|     std::string out; | ||||
|     for (u8 c : array) | ||||
|         out += fmt::format(upper ? "{:02X}" : "{:02x}", c); | ||||
|     return out; | ||||
| } | ||||
| 
 | ||||
| std::array<u8, 0x10> operator"" _array16(const char* str, size_t len); | ||||
| std::array<u8, 0x20> operator"" _array32(const char* str, size_t len); | ||||
| @ -20,6 +20,8 @@ add_library(core STATIC | ||||
|     crypto/key_manager.h | ||||
|     crypto/ctr_encryption_layer.cpp | ||||
|     crypto/ctr_encryption_layer.h | ||||
|     file_sys/bis_factory.cpp | ||||
|     file_sys/bis_factory.h | ||||
|     file_sys/card_image.cpp | ||||
|     file_sys/card_image.h | ||||
|     file_sys/content_archive.cpp | ||||
| @ -29,10 +31,14 @@ add_library(core STATIC | ||||
|     file_sys/directory.h | ||||
|     file_sys/errors.h | ||||
|     file_sys/mode.h | ||||
|     file_sys/nca_metadata.cpp | ||||
|     file_sys/nca_metadata.h | ||||
|     file_sys/partition_filesystem.cpp | ||||
|     file_sys/partition_filesystem.h | ||||
|     file_sys/program_metadata.cpp | ||||
|     file_sys/program_metadata.h | ||||
|     file_sys/registered_cache.cpp | ||||
|     file_sys/registered_cache.h | ||||
|     file_sys/romfs.cpp | ||||
|     file_sys/romfs.h | ||||
|     file_sys/romfs_factory.cpp | ||||
| @ -43,6 +49,8 @@ add_library(core STATIC | ||||
|     file_sys/sdmc_factory.h | ||||
|     file_sys/vfs.cpp | ||||
|     file_sys/vfs.h | ||||
|     file_sys/vfs_concat.cpp | ||||
|     file_sys/vfs_concat.h | ||||
|     file_sys/vfs_offset.cpp | ||||
|     file_sys/vfs_offset.h | ||||
|     file_sys/vfs_real.cpp | ||||
|  | ||||
| @ -5,6 +5,7 @@ | ||||
| #include <memory> | ||||
| #include <utility> | ||||
| #include "common/logging/log.h" | ||||
| #include "common/string_util.h" | ||||
| #include "core/core.h" | ||||
| #include "core/core_timing.h" | ||||
| #include "core/gdbstub/gdbstub.h" | ||||
| @ -17,6 +18,7 @@ | ||||
| #include "core/hle/service/sm/sm.h" | ||||
| #include "core/loader/loader.h" | ||||
| #include "core/settings.h" | ||||
| #include "file_sys/vfs_concat.h" | ||||
| #include "file_sys/vfs_real.h" | ||||
| #include "video_core/renderer_base.h" | ||||
| #include "video_core/video_core.h" | ||||
| @ -88,8 +90,39 @@ System::ResultStatus System::SingleStep() { | ||||
|     return RunLoop(false); | ||||
| } | ||||
| 
 | ||||
| static FileSys::VirtualFile GetGameFileFromPath(const FileSys::VirtualFilesystem& vfs, | ||||
|                                                 const std::string& path) { | ||||
|     // To account for split 00+01+etc files.
 | ||||
|     std::string dir_name; | ||||
|     std::string filename; | ||||
|     Common::SplitPath(path, &dir_name, &filename, nullptr); | ||||
|     if (filename == "00") { | ||||
|         const auto dir = vfs->OpenDirectory(dir_name, FileSys::Mode::Read); | ||||
|         std::vector<FileSys::VirtualFile> concat; | ||||
|         for (u8 i = 0; i < 0x10; ++i) { | ||||
|             auto next = dir->GetFile(fmt::format("{:02X}", i)); | ||||
|             if (next != nullptr) | ||||
|                 concat.push_back(std::move(next)); | ||||
|             else { | ||||
|                 next = dir->GetFile(fmt::format("{:02x}", i)); | ||||
|                 if (next != nullptr) | ||||
|                     concat.push_back(std::move(next)); | ||||
|                 else | ||||
|                     break; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (concat.empty()) | ||||
|             return nullptr; | ||||
| 
 | ||||
|         return FileSys::ConcatenateFiles(concat, dir->GetName()); | ||||
|     } | ||||
| 
 | ||||
|     return vfs->OpenFile(path, FileSys::Mode::Read); | ||||
| } | ||||
| 
 | ||||
| System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::string& filepath) { | ||||
|     app_loader = Loader::GetLoader(virtual_filesystem->OpenFile(filepath, FileSys::Mode::Read)); | ||||
|     app_loader = Loader::GetLoader(GetGameFileFromPath(virtual_filesystem, filepath)); | ||||
| 
 | ||||
|     if (!app_loader) { | ||||
|         LOG_CRITICAL(Core, "Failed to obtain loader for {}!", filepath); | ||||
|  | ||||
| @ -10,44 +10,13 @@ | ||||
| #include <string_view> | ||||
| #include "common/common_paths.h" | ||||
| #include "common/file_util.h" | ||||
| #include "common/hex_util.h" | ||||
| #include "common/logging/log.h" | ||||
| #include "core/crypto/key_manager.h" | ||||
| #include "core/settings.h" | ||||
| 
 | ||||
| namespace Core::Crypto { | ||||
| 
 | ||||
| static u8 ToHexNibble(char c1) { | ||||
|     if (c1 >= 65 && c1 <= 70) | ||||
|         return c1 - 55; | ||||
|     if (c1 >= 97 && c1 <= 102) | ||||
|         return c1 - 87; | ||||
|     if (c1 >= 48 && c1 <= 57) | ||||
|         return c1 - 48; | ||||
|     throw std::logic_error("Invalid hex digit"); | ||||
| } | ||||
| 
 | ||||
| template <size_t Size> | ||||
| static std::array<u8, Size> HexStringToArray(std::string_view str) { | ||||
|     std::array<u8, Size> out{}; | ||||
|     for (size_t i = 0; i < 2 * Size; i += 2) { | ||||
|         auto d1 = str[i]; | ||||
|         auto d2 = str[i + 1]; | ||||
|         out[i / 2] = (ToHexNibble(d1) << 4) | ToHexNibble(d2); | ||||
|     } | ||||
|     return out; | ||||
| } | ||||
| 
 | ||||
| std::array<u8, 16> operator""_array16(const char* str, size_t len) { | ||||
|     if (len != 32) | ||||
|         throw std::logic_error("Not of correct size."); | ||||
|     return HexStringToArray<16>(str); | ||||
| } | ||||
| 
 | ||||
| std::array<u8, 32> operator""_array32(const char* str, size_t len) { | ||||
|     if (len != 64) | ||||
|         throw std::logic_error("Not of correct size."); | ||||
|     return HexStringToArray<32>(str); | ||||
| } | ||||
| 
 | ||||
| KeyManager::KeyManager() { | ||||
|     // Initialize keys
 | ||||
|     const std::string hactool_keys_dir = FileUtil::GetHactoolConfigurationPath(); | ||||
|  | ||||
| @ -87,9 +87,6 @@ struct hash<Core::Crypto::KeyIndex<KeyType>> { | ||||
| 
 | ||||
| namespace Core::Crypto { | ||||
| 
 | ||||
| std::array<u8, 0x10> operator"" _array16(const char* str, size_t len); | ||||
| std::array<u8, 0x20> operator"" _array32(const char* str, size_t len); | ||||
| 
 | ||||
| class KeyManager { | ||||
| public: | ||||
|     KeyManager(); | ||||
|  | ||||
							
								
								
									
										31
									
								
								src/core/file_sys/bis_factory.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/core/file_sys/bis_factory.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,31 @@ | ||||
| // Copyright 2018 yuzu emulator team
 | ||||
| // Licensed under GPLv2 or any later version
 | ||||
| // Refer to the license.txt file included.
 | ||||
| 
 | ||||
| #include "core/file_sys/bis_factory.h" | ||||
| 
 | ||||
| namespace FileSys { | ||||
| 
 | ||||
| static VirtualDir GetOrCreateDirectory(const VirtualDir& dir, std::string_view path) { | ||||
|     const auto res = dir->GetDirectoryRelative(path); | ||||
|     if (res == nullptr) | ||||
|         return dir->CreateDirectoryRelative(path); | ||||
|     return res; | ||||
| } | ||||
| 
 | ||||
| BISFactory::BISFactory(VirtualDir nand_root_) | ||||
|     : nand_root(std::move(nand_root_)), | ||||
|       sysnand_cache(std::make_shared<RegisteredCache>( | ||||
|           GetOrCreateDirectory(nand_root, "/system/Contents/registered"))), | ||||
|       usrnand_cache(std::make_shared<RegisteredCache>( | ||||
|           GetOrCreateDirectory(nand_root, "/user/Contents/registered"))) {} | ||||
| 
 | ||||
| std::shared_ptr<RegisteredCache> BISFactory::GetSystemNANDContents() const { | ||||
|     return sysnand_cache; | ||||
| } | ||||
| 
 | ||||
| std::shared_ptr<RegisteredCache> BISFactory::GetUserNANDContents() const { | ||||
|     return usrnand_cache; | ||||
| } | ||||
| 
 | ||||
| } // namespace FileSys
 | ||||
							
								
								
									
										30
									
								
								src/core/file_sys/bis_factory.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/core/file_sys/bis_factory.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,30 @@ | ||||
| // Copyright 2018 yuzu emulator team
 | ||||
| // Licensed under GPLv2 or any later version
 | ||||
| // Refer to the license.txt file included.
 | ||||
| 
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include <memory> | ||||
| #include "core/loader/loader.h" | ||||
| #include "registered_cache.h" | ||||
| 
 | ||||
| namespace FileSys { | ||||
| 
 | ||||
| /// File system interface to the Built-In Storage
 | ||||
| /// This is currently missing accessors to BIS partitions, but seemed like a good place for the NAND
 | ||||
| /// registered caches.
 | ||||
| class BISFactory { | ||||
| public: | ||||
|     explicit BISFactory(VirtualDir nand_root); | ||||
| 
 | ||||
|     std::shared_ptr<RegisteredCache> GetSystemNANDContents() const; | ||||
|     std::shared_ptr<RegisteredCache> GetUserNANDContents() const; | ||||
| 
 | ||||
| private: | ||||
|     VirtualDir nand_root; | ||||
| 
 | ||||
|     std::shared_ptr<RegisteredCache> sysnand_cache; | ||||
|     std::shared_ptr<RegisteredCache> usrnand_cache; | ||||
| }; | ||||
| 
 | ||||
| } // namespace FileSys
 | ||||
| @ -96,6 +96,10 @@ VirtualDir XCI::GetLogoPartition() const { | ||||
|     return GetPartition(XCIPartition::Logo); | ||||
| } | ||||
| 
 | ||||
| const std::vector<std::shared_ptr<NCA>>& XCI::GetNCAs() const { | ||||
|     return ncas; | ||||
| } | ||||
| 
 | ||||
| std::shared_ptr<NCA> XCI::GetNCAByType(NCAContentType type) const { | ||||
|     const auto iter = | ||||
|         std::find_if(ncas.begin(), ncas.end(), | ||||
|  | ||||
| @ -68,6 +68,7 @@ public: | ||||
|     VirtualDir GetUpdatePartition() const; | ||||
|     VirtualDir GetLogoPartition() const; | ||||
| 
 | ||||
|     const std::vector<std::shared_ptr<NCA>>& GetNCAs() const; | ||||
|     std::shared_ptr<NCA> GetNCAByType(NCAContentType type) const; | ||||
|     VirtualFile GetNCAFileByType(NCAContentType type) const; | ||||
| 
 | ||||
|  | ||||
| @ -16,7 +16,7 @@ std::string LanguageEntry::GetDeveloperName() const { | ||||
|     return Common::StringFromFixedZeroTerminatedBuffer(developer_name.data(), 0x100); | ||||
| } | ||||
| 
 | ||||
| NACP::NACP(VirtualFile file_) : file(std::move(file_)), raw(std::make_unique<RawNACP>()) { | ||||
| NACP::NACP(VirtualFile file) : raw(std::make_unique<RawNACP>()) { | ||||
|     file->ReadObject(raw.get()); | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -81,7 +81,6 @@ public: | ||||
|     std::string GetVersionString() const; | ||||
| 
 | ||||
| private: | ||||
|     VirtualFile file; | ||||
|     std::unique_ptr<RawNACP> raw; | ||||
| }; | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										131
									
								
								src/core/file_sys/nca_metadata.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								src/core/file_sys/nca_metadata.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,131 @@ | ||||
| // Copyright 2018 yuzu emulator team
 | ||||
| // Licensed under GPLv2 or any later version
 | ||||
| // Refer to the license.txt file included.
 | ||||
| 
 | ||||
| #include <cstring> | ||||
| #include "common/common_funcs.h" | ||||
| #include "common/logging/log.h" | ||||
| #include "common/swap.h" | ||||
| #include "content_archive.h" | ||||
| #include "core/file_sys/nca_metadata.h" | ||||
| 
 | ||||
| namespace FileSys { | ||||
| 
 | ||||
| bool operator>=(TitleType lhs, TitleType rhs) { | ||||
|     return static_cast<size_t>(lhs) >= static_cast<size_t>(rhs); | ||||
| } | ||||
| 
 | ||||
| bool operator<=(TitleType lhs, TitleType rhs) { | ||||
|     return static_cast<size_t>(lhs) <= static_cast<size_t>(rhs); | ||||
| } | ||||
| 
 | ||||
| CNMT::CNMT(VirtualFile file) { | ||||
|     if (file->ReadObject(&header) != sizeof(CNMTHeader)) | ||||
|         return; | ||||
| 
 | ||||
|     // If type is {Application, Update, AOC} has opt-header.
 | ||||
|     if (header.type >= TitleType::Application && header.type <= TitleType::AOC) { | ||||
|         if (file->ReadObject(&opt_header, sizeof(CNMTHeader)) != sizeof(OptionalHeader)) { | ||||
|             LOG_WARNING(Loader, "Failed to read optional header."); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     for (u16 i = 0; i < header.number_content_entries; ++i) { | ||||
|         auto& next = content_records.emplace_back(ContentRecord{}); | ||||
|         if (file->ReadObject(&next, sizeof(CNMTHeader) + i * sizeof(ContentRecord) + | ||||
|                                         header.table_offset) != sizeof(ContentRecord)) { | ||||
|             content_records.erase(content_records.end() - 1); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     for (u16 i = 0; i < header.number_meta_entries; ++i) { | ||||
|         auto& next = meta_records.emplace_back(MetaRecord{}); | ||||
|         if (file->ReadObject(&next, sizeof(CNMTHeader) + i * sizeof(MetaRecord) + | ||||
|                                         header.table_offset) != sizeof(MetaRecord)) { | ||||
|             meta_records.erase(meta_records.end() - 1); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| CNMT::CNMT(CNMTHeader header, OptionalHeader opt_header, std::vector<ContentRecord> content_records, | ||||
|            std::vector<MetaRecord> meta_records) | ||||
|     : header(std::move(header)), opt_header(std::move(opt_header)), | ||||
|       content_records(std::move(content_records)), meta_records(std::move(meta_records)) {} | ||||
| 
 | ||||
| u64 CNMT::GetTitleID() const { | ||||
|     return header.title_id; | ||||
| } | ||||
| 
 | ||||
| u32 CNMT::GetTitleVersion() const { | ||||
|     return header.title_version; | ||||
| } | ||||
| 
 | ||||
| TitleType CNMT::GetType() const { | ||||
|     return header.type; | ||||
| } | ||||
| 
 | ||||
| const std::vector<ContentRecord>& CNMT::GetContentRecords() const { | ||||
|     return content_records; | ||||
| } | ||||
| 
 | ||||
| const std::vector<MetaRecord>& CNMT::GetMetaRecords() const { | ||||
|     return meta_records; | ||||
| } | ||||
| 
 | ||||
| bool CNMT::UnionRecords(const CNMT& other) { | ||||
|     bool change = false; | ||||
|     for (const auto& rec : other.content_records) { | ||||
|         const auto iter = std::find_if(content_records.begin(), content_records.end(), | ||||
|                                        [&rec](const ContentRecord& r) { | ||||
|                                            return r.nca_id == rec.nca_id && r.type == rec.type; | ||||
|                                        }); | ||||
|         if (iter == content_records.end()) { | ||||
|             content_records.emplace_back(rec); | ||||
|             ++header.number_content_entries; | ||||
|             change = true; | ||||
|         } | ||||
|     } | ||||
|     for (const auto& rec : other.meta_records) { | ||||
|         const auto iter = | ||||
|             std::find_if(meta_records.begin(), meta_records.end(), [&rec](const MetaRecord& r) { | ||||
|                 return r.title_id == rec.title_id && r.title_version == rec.title_version && | ||||
|                        r.type == rec.type; | ||||
|             }); | ||||
|         if (iter == meta_records.end()) { | ||||
|             meta_records.emplace_back(rec); | ||||
|             ++header.number_meta_entries; | ||||
|             change = true; | ||||
|         } | ||||
|     } | ||||
|     return change; | ||||
| } | ||||
| 
 | ||||
| std::vector<u8> CNMT::Serialize() const { | ||||
|     const bool has_opt_header = | ||||
|         header.type >= TitleType::Application && header.type <= TitleType::AOC; | ||||
|     const auto dead_zone = header.table_offset + sizeof(CNMTHeader); | ||||
|     std::vector<u8> out( | ||||
|         std::max(sizeof(CNMTHeader) + (has_opt_header ? sizeof(OptionalHeader) : 0), dead_zone) + | ||||
|         content_records.size() * sizeof(ContentRecord) + meta_records.size() * sizeof(MetaRecord)); | ||||
|     memcpy(out.data(), &header, sizeof(CNMTHeader)); | ||||
| 
 | ||||
|     // Optional Header
 | ||||
|     if (has_opt_header) { | ||||
|         memcpy(out.data() + sizeof(CNMTHeader), &opt_header, sizeof(OptionalHeader)); | ||||
|     } | ||||
| 
 | ||||
|     auto offset = header.table_offset; | ||||
| 
 | ||||
|     for (const auto& rec : content_records) { | ||||
|         memcpy(out.data() + offset + sizeof(CNMTHeader), &rec, sizeof(ContentRecord)); | ||||
|         offset += sizeof(ContentRecord); | ||||
|     } | ||||
| 
 | ||||
|     for (const auto& rec : meta_records) { | ||||
|         memcpy(out.data() + offset + sizeof(CNMTHeader), &rec, sizeof(MetaRecord)); | ||||
|         offset += sizeof(MetaRecord); | ||||
|     } | ||||
| 
 | ||||
|     return out; | ||||
| } | ||||
| } // namespace FileSys
 | ||||
							
								
								
									
										111
									
								
								src/core/file_sys/nca_metadata.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								src/core/file_sys/nca_metadata.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,111 @@ | ||||
| // Copyright 2018 yuzu emulator team
 | ||||
| // Licensed under GPLv2 or any later version
 | ||||
| // Refer to the license.txt file included.
 | ||||
| 
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include <cstring> | ||||
| #include <memory> | ||||
| #include <vector> | ||||
| #include "common/common_types.h" | ||||
| #include "common/swap.h" | ||||
| #include "core/file_sys/vfs.h" | ||||
| 
 | ||||
| namespace FileSys { | ||||
| class CNMT; | ||||
| 
 | ||||
| struct CNMTHeader; | ||||
| struct OptionalHeader; | ||||
| 
 | ||||
| enum class TitleType : u8 { | ||||
|     SystemProgram = 0x01, | ||||
|     SystemDataArchive = 0x02, | ||||
|     SystemUpdate = 0x03, | ||||
|     FirmwarePackageA = 0x04, | ||||
|     FirmwarePackageB = 0x05, | ||||
|     Application = 0x80, | ||||
|     Update = 0x81, | ||||
|     AOC = 0x82, | ||||
|     DeltaTitle = 0x83, | ||||
| }; | ||||
| 
 | ||||
| bool operator>=(TitleType lhs, TitleType rhs); | ||||
| bool operator<=(TitleType lhs, TitleType rhs); | ||||
| 
 | ||||
| enum class ContentRecordType : u8 { | ||||
|     Meta = 0, | ||||
|     Program = 1, | ||||
|     Data = 2, | ||||
|     Control = 3, | ||||
|     Manual = 4, | ||||
|     Legal = 5, | ||||
|     Patch = 6, | ||||
| }; | ||||
| 
 | ||||
| struct ContentRecord { | ||||
|     std::array<u8, 0x20> hash; | ||||
|     std::array<u8, 0x10> nca_id; | ||||
|     std::array<u8, 0x6> size; | ||||
|     ContentRecordType type; | ||||
|     INSERT_PADDING_BYTES(1); | ||||
| }; | ||||
| static_assert(sizeof(ContentRecord) == 0x38, "ContentRecord has incorrect size."); | ||||
| 
 | ||||
| constexpr ContentRecord EMPTY_META_CONTENT_RECORD{{}, {}, {}, ContentRecordType::Meta, {}}; | ||||
| 
 | ||||
| struct MetaRecord { | ||||
|     u64_le title_id; | ||||
|     u32_le title_version; | ||||
|     TitleType type; | ||||
|     u8 install_byte; | ||||
|     INSERT_PADDING_BYTES(2); | ||||
| }; | ||||
| static_assert(sizeof(MetaRecord) == 0x10, "MetaRecord has incorrect size."); | ||||
| 
 | ||||
| struct OptionalHeader { | ||||
|     u64_le title_id; | ||||
|     u64_le minimum_version; | ||||
| }; | ||||
| static_assert(sizeof(OptionalHeader) == 0x10, "OptionalHeader has incorrect size."); | ||||
| 
 | ||||
| struct CNMTHeader { | ||||
|     u64_le title_id; | ||||
|     u32_le title_version; | ||||
|     TitleType type; | ||||
|     INSERT_PADDING_BYTES(1); | ||||
|     u16_le table_offset; | ||||
|     u16_le number_content_entries; | ||||
|     u16_le number_meta_entries; | ||||
|     INSERT_PADDING_BYTES(12); | ||||
| }; | ||||
| static_assert(sizeof(CNMTHeader) == 0x20, "CNMTHeader has incorrect size."); | ||||
| 
 | ||||
| // A class representing the format used by NCA metadata files, typically named {}.cnmt.nca or
 | ||||
| // meta0.ncd. These describe which NCA's belong with which titles in the registered cache.
 | ||||
| class CNMT { | ||||
| public: | ||||
|     explicit CNMT(VirtualFile file); | ||||
|     CNMT(CNMTHeader header, OptionalHeader opt_header, std::vector<ContentRecord> content_records, | ||||
|          std::vector<MetaRecord> meta_records); | ||||
| 
 | ||||
|     u64 GetTitleID() const; | ||||
|     u32 GetTitleVersion() const; | ||||
|     TitleType GetType() const; | ||||
| 
 | ||||
|     const std::vector<ContentRecord>& GetContentRecords() const; | ||||
|     const std::vector<MetaRecord>& GetMetaRecords() const; | ||||
| 
 | ||||
|     bool UnionRecords(const CNMT& other); | ||||
|     std::vector<u8> Serialize() const; | ||||
| 
 | ||||
| private: | ||||
|     CNMTHeader header; | ||||
|     OptionalHeader opt_header; | ||||
|     std::vector<ContentRecord> content_records; | ||||
|     std::vector<MetaRecord> meta_records; | ||||
| 
 | ||||
|     // TODO(DarkLordZach): According to switchbrew, for Patch-type there is additional data
 | ||||
|     // after the table. This is not documented, unfortunately.
 | ||||
| }; | ||||
| 
 | ||||
| } // namespace FileSys
 | ||||
							
								
								
									
										476
									
								
								src/core/file_sys/registered_cache.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										476
									
								
								src/core/file_sys/registered_cache.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,476 @@ | ||||
| // Copyright 2018 yuzu emulator team
 | ||||
| // Licensed under GPLv2 or any later version
 | ||||
| // Refer to the license.txt file included.
 | ||||
| 
 | ||||
| #include <regex> | ||||
| #include <mbedtls/sha256.h> | ||||
| #include "common/assert.h" | ||||
| #include "common/hex_util.h" | ||||
| #include "common/logging/log.h" | ||||
| #include "core/crypto/encryption_layer.h" | ||||
| #include "core/file_sys/card_image.h" | ||||
| #include "core/file_sys/nca_metadata.h" | ||||
| #include "core/file_sys/registered_cache.h" | ||||
| #include "core/file_sys/vfs_concat.h" | ||||
| 
 | ||||
| namespace FileSys { | ||||
| std::string RegisteredCacheEntry::DebugInfo() const { | ||||
|     return fmt::format("title_id={:016X}, content_type={:02X}", title_id, static_cast<u8>(type)); | ||||
| } | ||||
| 
 | ||||
| bool operator<(const RegisteredCacheEntry& lhs, const RegisteredCacheEntry& rhs) { | ||||
|     return (lhs.title_id < rhs.title_id) || (lhs.title_id == rhs.title_id && lhs.type < rhs.type); | ||||
| } | ||||
| 
 | ||||
| static bool FollowsTwoDigitDirFormat(std::string_view name) { | ||||
|     static const std::regex two_digit_regex("000000[0-9A-F]{2}", std::regex_constants::ECMAScript | | ||||
|                                                                      std::regex_constants::icase); | ||||
|     return std::regex_match(name.begin(), name.end(), two_digit_regex); | ||||
| } | ||||
| 
 | ||||
| static bool FollowsNcaIdFormat(std::string_view name) { | ||||
|     static const std::regex nca_id_regex("[0-9A-F]{32}\\.nca", std::regex_constants::ECMAScript | | ||||
|                                                                    std::regex_constants::icase); | ||||
|     return name.size() == 36 && std::regex_match(name.begin(), name.end(), nca_id_regex); | ||||
| } | ||||
| 
 | ||||
| static std::string GetRelativePathFromNcaID(const std::array<u8, 16>& nca_id, bool second_hex_upper, | ||||
|                                             bool within_two_digit) { | ||||
|     if (!within_two_digit) | ||||
|         return fmt::format("/{}.nca", HexArrayToString(nca_id, second_hex_upper)); | ||||
| 
 | ||||
|     Core::Crypto::SHA256Hash hash{}; | ||||
|     mbedtls_sha256(nca_id.data(), nca_id.size(), hash.data(), 0); | ||||
|     return fmt::format("/000000{:02X}/{}.nca", hash[0], HexArrayToString(nca_id, second_hex_upper)); | ||||
| } | ||||
| 
 | ||||
| static std::string GetCNMTName(TitleType type, u64 title_id) { | ||||
|     constexpr std::array<const char*, 9> TITLE_TYPE_NAMES{ | ||||
|         "SystemProgram", | ||||
|         "SystemData", | ||||
|         "SystemUpdate", | ||||
|         "BootImagePackage", | ||||
|         "BootImagePackageSafe", | ||||
|         "Application", | ||||
|         "Patch", | ||||
|         "AddOnContent", | ||||
|         "" ///< Currently unknown 'DeltaTitle'
 | ||||
|     }; | ||||
| 
 | ||||
|     auto index = static_cast<size_t>(type); | ||||
|     // If the index is after the jump in TitleType, subtract it out.
 | ||||
|     if (index >= static_cast<size_t>(TitleType::Application)) { | ||||
|         index -= static_cast<size_t>(TitleType::Application) - | ||||
|                  static_cast<size_t>(TitleType::FirmwarePackageB); | ||||
|     } | ||||
|     return fmt::format("{}_{:016x}.cnmt", TITLE_TYPE_NAMES[index], title_id); | ||||
| } | ||||
| 
 | ||||
| static ContentRecordType GetCRTypeFromNCAType(NCAContentType type) { | ||||
|     switch (type) { | ||||
|     case NCAContentType::Program: | ||||
|         // TODO(DarkLordZach): Differentiate between Program and Patch
 | ||||
|         return ContentRecordType::Program; | ||||
|     case NCAContentType::Meta: | ||||
|         return ContentRecordType::Meta; | ||||
|     case NCAContentType::Control: | ||||
|         return ContentRecordType::Control; | ||||
|     case NCAContentType::Data: | ||||
|         return ContentRecordType::Data; | ||||
|     case NCAContentType::Manual: | ||||
|         // TODO(DarkLordZach): Peek at NCA contents to differentiate Manual and Legal.
 | ||||
|         return ContentRecordType::Manual; | ||||
|     default: | ||||
|         UNREACHABLE(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| VirtualFile RegisteredCache::OpenFileOrDirectoryConcat(const VirtualDir& dir, | ||||
|                                                        std::string_view path) const { | ||||
|     if (dir->GetFileRelative(path) != nullptr) | ||||
|         return dir->GetFileRelative(path); | ||||
|     if (dir->GetDirectoryRelative(path) != nullptr) { | ||||
|         const auto nca_dir = dir->GetDirectoryRelative(path); | ||||
|         VirtualFile file = nullptr; | ||||
| 
 | ||||
|         const auto files = nca_dir->GetFiles(); | ||||
|         if (files.size() == 1 && files[0]->GetName() == "00") { | ||||
|             file = files[0]; | ||||
|         } else { | ||||
|             std::vector<VirtualFile> concat; | ||||
|             // Since the files are a two-digit hex number, max is FF.
 | ||||
|             for (size_t i = 0; i < 0x100; ++i) { | ||||
|                 auto next = nca_dir->GetFile(fmt::format("{:02X}", i)); | ||||
|                 if (next != nullptr) { | ||||
|                     concat.push_back(std::move(next)); | ||||
|                 } else { | ||||
|                     next = nca_dir->GetFile(fmt::format("{:02x}", i)); | ||||
|                     if (next != nullptr) | ||||
|                         concat.push_back(std::move(next)); | ||||
|                     else | ||||
|                         break; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (concat.empty()) | ||||
|                 return nullptr; | ||||
| 
 | ||||
|             file = FileSys::ConcatenateFiles(concat); | ||||
|         } | ||||
| 
 | ||||
|         return file; | ||||
|     } | ||||
|     return nullptr; | ||||
| } | ||||
| 
 | ||||
| VirtualFile RegisteredCache::GetFileAtID(NcaID id) const { | ||||
|     VirtualFile file; | ||||
|     // Try all four modes of file storage:
 | ||||
|     // (bit 1 = uppercase/lower, bit 0 = within a two-digit dir)
 | ||||
|     // 00: /000000**/{:032X}.nca
 | ||||
|     // 01: /{:032X}.nca
 | ||||
|     // 10: /000000**/{:032x}.nca
 | ||||
|     // 11: /{:032x}.nca
 | ||||
|     for (u8 i = 0; i < 4; ++i) { | ||||
|         const auto path = GetRelativePathFromNcaID(id, (i & 0b10) == 0, (i & 0b01) == 0); | ||||
|         file = OpenFileOrDirectoryConcat(dir, path); | ||||
|         if (file != nullptr) | ||||
|             return file; | ||||
|     } | ||||
|     return file; | ||||
| } | ||||
| 
 | ||||
| static boost::optional<NcaID> CheckMapForContentRecord( | ||||
|     const boost::container::flat_map<u64, CNMT>& map, u64 title_id, ContentRecordType type) { | ||||
|     if (map.find(title_id) == map.end()) | ||||
|         return boost::none; | ||||
| 
 | ||||
|     const auto& cnmt = map.at(title_id); | ||||
| 
 | ||||
|     const auto iter = std::find_if(cnmt.GetContentRecords().begin(), cnmt.GetContentRecords().end(), | ||||
|                                    [type](const ContentRecord& rec) { return rec.type == type; }); | ||||
|     if (iter == cnmt.GetContentRecords().end()) | ||||
|         return boost::none; | ||||
| 
 | ||||
|     return boost::make_optional(iter->nca_id); | ||||
| } | ||||
| 
 | ||||
| boost::optional<NcaID> RegisteredCache::GetNcaIDFromMetadata(u64 title_id, | ||||
|                                                              ContentRecordType type) const { | ||||
|     if (type == ContentRecordType::Meta && meta_id.find(title_id) != meta_id.end()) | ||||
|         return meta_id.at(title_id); | ||||
| 
 | ||||
|     const auto res1 = CheckMapForContentRecord(yuzu_meta, title_id, type); | ||||
|     if (res1 != boost::none) | ||||
|         return res1; | ||||
|     return CheckMapForContentRecord(meta, title_id, type); | ||||
| } | ||||
| 
 | ||||
| std::vector<NcaID> RegisteredCache::AccumulateFiles() const { | ||||
|     std::vector<NcaID> ids; | ||||
|     for (const auto& d2_dir : dir->GetSubdirectories()) { | ||||
|         if (FollowsNcaIdFormat(d2_dir->GetName())) { | ||||
|             ids.push_back(HexStringToArray<0x10, true>(d2_dir->GetName().substr(0, 0x20))); | ||||
|             continue; | ||||
|         } | ||||
| 
 | ||||
|         if (!FollowsTwoDigitDirFormat(d2_dir->GetName())) | ||||
|             continue; | ||||
| 
 | ||||
|         for (const auto& nca_dir : d2_dir->GetSubdirectories()) { | ||||
|             if (!FollowsNcaIdFormat(nca_dir->GetName())) | ||||
|                 continue; | ||||
| 
 | ||||
|             ids.push_back(HexStringToArray<0x10, true>(nca_dir->GetName().substr(0, 0x20))); | ||||
|         } | ||||
| 
 | ||||
|         for (const auto& nca_file : d2_dir->GetFiles()) { | ||||
|             if (!FollowsNcaIdFormat(nca_file->GetName())) | ||||
|                 continue; | ||||
| 
 | ||||
|             ids.push_back(HexStringToArray<0x10, true>(nca_file->GetName().substr(0, 0x20))); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     for (const auto& d2_file : dir->GetFiles()) { | ||||
|         if (FollowsNcaIdFormat(d2_file->GetName())) | ||||
|             ids.push_back(HexStringToArray<0x10, true>(d2_file->GetName().substr(0, 0x20))); | ||||
|     } | ||||
|     return ids; | ||||
| } | ||||
| 
 | ||||
| void RegisteredCache::ProcessFiles(const std::vector<NcaID>& ids) { | ||||
|     for (const auto& id : ids) { | ||||
|         const auto file = GetFileAtID(id); | ||||
| 
 | ||||
|         if (file == nullptr) | ||||
|             continue; | ||||
|         const auto nca = std::make_shared<NCA>(parser(file, id)); | ||||
|         if (nca->GetStatus() != Loader::ResultStatus::Success || | ||||
|             nca->GetType() != NCAContentType::Meta) { | ||||
|             continue; | ||||
|         } | ||||
| 
 | ||||
|         const auto section0 = nca->GetSubdirectories()[0]; | ||||
| 
 | ||||
|         for (const auto& file : section0->GetFiles()) { | ||||
|             if (file->GetExtension() != "cnmt") | ||||
|                 continue; | ||||
| 
 | ||||
|             meta.insert_or_assign(nca->GetTitleId(), CNMT(file)); | ||||
|             meta_id.insert_or_assign(nca->GetTitleId(), id); | ||||
|             break; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void RegisteredCache::AccumulateYuzuMeta() { | ||||
|     const auto dir = this->dir->GetSubdirectory("yuzu_meta"); | ||||
|     if (dir == nullptr) | ||||
|         return; | ||||
| 
 | ||||
|     for (const auto& file : dir->GetFiles()) { | ||||
|         if (file->GetExtension() != "cnmt") | ||||
|             continue; | ||||
| 
 | ||||
|         CNMT cnmt(file); | ||||
|         yuzu_meta.insert_or_assign(cnmt.GetTitleID(), std::move(cnmt)); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void RegisteredCache::Refresh() { | ||||
|     if (dir == nullptr) | ||||
|         return; | ||||
|     const auto ids = AccumulateFiles(); | ||||
|     ProcessFiles(ids); | ||||
|     AccumulateYuzuMeta(); | ||||
| } | ||||
| 
 | ||||
| RegisteredCache::RegisteredCache(VirtualDir dir_, RegisteredCacheParsingFunction parsing_function) | ||||
|     : dir(std::move(dir_)), parser(std::move(parsing_function)) { | ||||
|     Refresh(); | ||||
| } | ||||
| 
 | ||||
| bool RegisteredCache::HasEntry(u64 title_id, ContentRecordType type) const { | ||||
|     return GetEntryRaw(title_id, type) != nullptr; | ||||
| } | ||||
| 
 | ||||
| bool RegisteredCache::HasEntry(RegisteredCacheEntry entry) const { | ||||
|     return GetEntryRaw(entry) != nullptr; | ||||
| } | ||||
| 
 | ||||
| VirtualFile RegisteredCache::GetEntryRaw(u64 title_id, ContentRecordType type) const { | ||||
|     const auto id = GetNcaIDFromMetadata(title_id, type); | ||||
|     if (id == boost::none) | ||||
|         return nullptr; | ||||
| 
 | ||||
|     return parser(GetFileAtID(id.get()), id.get()); | ||||
| } | ||||
| 
 | ||||
| VirtualFile RegisteredCache::GetEntryRaw(RegisteredCacheEntry entry) const { | ||||
|     return GetEntryRaw(entry.title_id, entry.type); | ||||
| } | ||||
| 
 | ||||
| std::shared_ptr<NCA> RegisteredCache::GetEntry(u64 title_id, ContentRecordType type) const { | ||||
|     const auto raw = GetEntryRaw(title_id, type); | ||||
|     if (raw == nullptr) | ||||
|         return nullptr; | ||||
|     return std::make_shared<NCA>(raw); | ||||
| } | ||||
| 
 | ||||
| std::shared_ptr<NCA> RegisteredCache::GetEntry(RegisteredCacheEntry entry) const { | ||||
|     return GetEntry(entry.title_id, entry.type); | ||||
| } | ||||
| 
 | ||||
| template <typename T> | ||||
| void RegisteredCache::IterateAllMetadata( | ||||
|     std::vector<T>& out, std::function<T(const CNMT&, const ContentRecord&)> proc, | ||||
|     std::function<bool(const CNMT&, const ContentRecord&)> filter) const { | ||||
|     for (const auto& kv : meta) { | ||||
|         const auto& cnmt = kv.second; | ||||
|         if (filter(cnmt, EMPTY_META_CONTENT_RECORD)) | ||||
|             out.push_back(proc(cnmt, EMPTY_META_CONTENT_RECORD)); | ||||
|         for (const auto& rec : cnmt.GetContentRecords()) { | ||||
|             if (GetFileAtID(rec.nca_id) != nullptr && filter(cnmt, rec)) { | ||||
|                 out.push_back(proc(cnmt, rec)); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     for (const auto& kv : yuzu_meta) { | ||||
|         const auto& cnmt = kv.second; | ||||
|         for (const auto& rec : cnmt.GetContentRecords()) { | ||||
|             if (GetFileAtID(rec.nca_id) != nullptr && filter(cnmt, rec)) { | ||||
|                 out.push_back(proc(cnmt, rec)); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| std::vector<RegisteredCacheEntry> RegisteredCache::ListEntries() const { | ||||
|     std::vector<RegisteredCacheEntry> out; | ||||
|     IterateAllMetadata<RegisteredCacheEntry>( | ||||
|         out, | ||||
|         [](const CNMT& c, const ContentRecord& r) { | ||||
|             return RegisteredCacheEntry{c.GetTitleID(), r.type}; | ||||
|         }, | ||||
|         [](const CNMT& c, const ContentRecord& r) { return true; }); | ||||
|     return out; | ||||
| } | ||||
| 
 | ||||
| std::vector<RegisteredCacheEntry> RegisteredCache::ListEntriesFilter( | ||||
|     boost::optional<TitleType> title_type, boost::optional<ContentRecordType> record_type, | ||||
|     boost::optional<u64> title_id) const { | ||||
|     std::vector<RegisteredCacheEntry> out; | ||||
|     IterateAllMetadata<RegisteredCacheEntry>( | ||||
|         out, | ||||
|         [](const CNMT& c, const ContentRecord& r) { | ||||
|             return RegisteredCacheEntry{c.GetTitleID(), r.type}; | ||||
|         }, | ||||
|         [&title_type, &record_type, &title_id](const CNMT& c, const ContentRecord& r) { | ||||
|             if (title_type != boost::none && title_type.get() != c.GetType()) | ||||
|                 return false; | ||||
|             if (record_type != boost::none && record_type.get() != r.type) | ||||
|                 return false; | ||||
|             if (title_id != boost::none && title_id.get() != c.GetTitleID()) | ||||
|                 return false; | ||||
|             return true; | ||||
|         }); | ||||
|     return out; | ||||
| } | ||||
| 
 | ||||
| static std::shared_ptr<NCA> GetNCAFromXCIForID(std::shared_ptr<XCI> xci, const NcaID& id) { | ||||
|     const auto filename = fmt::format("{}.nca", HexArrayToString(id, false)); | ||||
|     const auto iter = | ||||
|         std::find_if(xci->GetNCAs().begin(), xci->GetNCAs().end(), | ||||
|                      [&filename](std::shared_ptr<NCA> nca) { return nca->GetName() == filename; }); | ||||
|     return iter == xci->GetNCAs().end() ? nullptr : *iter; | ||||
| } | ||||
| 
 | ||||
| InstallResult RegisteredCache::InstallEntry(std::shared_ptr<XCI> xci, bool overwrite_if_exists, | ||||
|                                             const VfsCopyFunction& copy) { | ||||
|     const auto& ncas = xci->GetNCAs(); | ||||
|     const auto& meta_iter = std::find_if(ncas.begin(), ncas.end(), [](std::shared_ptr<NCA> nca) { | ||||
|         return nca->GetType() == NCAContentType::Meta; | ||||
|     }); | ||||
| 
 | ||||
|     if (meta_iter == ncas.end()) { | ||||
|         LOG_ERROR(Loader, "The XCI you are attempting to install does not have a metadata NCA and " | ||||
|                           "is therefore malformed. Double check your encryption keys."); | ||||
|         return InstallResult::ErrorMetaFailed; | ||||
|     } | ||||
| 
 | ||||
|     // Install Metadata File
 | ||||
|     const auto meta_id_raw = (*meta_iter)->GetName().substr(0, 32); | ||||
|     const auto meta_id = HexStringToArray<16>(meta_id_raw); | ||||
| 
 | ||||
|     const auto res = RawInstallNCA(*meta_iter, copy, overwrite_if_exists, meta_id); | ||||
|     if (res != InstallResult::Success) | ||||
|         return res; | ||||
| 
 | ||||
|     // Install all the other NCAs
 | ||||
|     const auto section0 = (*meta_iter)->GetSubdirectories()[0]; | ||||
|     const auto cnmt_file = section0->GetFiles()[0]; | ||||
|     const CNMT cnmt(cnmt_file); | ||||
|     for (const auto& record : cnmt.GetContentRecords()) { | ||||
|         const auto nca = GetNCAFromXCIForID(xci, record.nca_id); | ||||
|         if (nca == nullptr) | ||||
|             return InstallResult::ErrorCopyFailed; | ||||
|         const auto res2 = RawInstallNCA(nca, copy, overwrite_if_exists, record.nca_id); | ||||
|         if (res2 != InstallResult::Success) | ||||
|             return res2; | ||||
|     } | ||||
| 
 | ||||
|     Refresh(); | ||||
|     return InstallResult::Success; | ||||
| } | ||||
| 
 | ||||
| InstallResult RegisteredCache::InstallEntry(std::shared_ptr<NCA> nca, TitleType type, | ||||
|                                             bool overwrite_if_exists, const VfsCopyFunction& copy) { | ||||
|     CNMTHeader header{ | ||||
|         nca->GetTitleId(), ///< Title ID
 | ||||
|         0,                 ///< Ignore/Default title version
 | ||||
|         type,              ///< Type
 | ||||
|         {},                ///< Padding
 | ||||
|         0x10,              ///< Default table offset
 | ||||
|         1,                 ///< 1 Content Entry
 | ||||
|         0,                 ///< No Meta Entries
 | ||||
|         {},                ///< Padding
 | ||||
|     }; | ||||
|     OptionalHeader opt_header{0, 0}; | ||||
|     ContentRecord c_rec{{}, {}, {}, GetCRTypeFromNCAType(nca->GetType()), {}}; | ||||
|     const auto& data = nca->GetBaseFile()->ReadBytes(0x100000); | ||||
|     mbedtls_sha256(data.data(), data.size(), c_rec.hash.data(), 0); | ||||
|     memcpy(&c_rec.nca_id, &c_rec.hash, 16); | ||||
|     const CNMT new_cnmt(header, opt_header, {c_rec}, {}); | ||||
|     if (!RawInstallYuzuMeta(new_cnmt)) | ||||
|         return InstallResult::ErrorMetaFailed; | ||||
|     return RawInstallNCA(nca, copy, overwrite_if_exists, c_rec.nca_id); | ||||
| } | ||||
| 
 | ||||
| InstallResult RegisteredCache::RawInstallNCA(std::shared_ptr<NCA> nca, const VfsCopyFunction& copy, | ||||
|                                              bool overwrite_if_exists, | ||||
|                                              boost::optional<NcaID> override_id) { | ||||
|     const auto in = nca->GetBaseFile(); | ||||
|     Core::Crypto::SHA256Hash hash{}; | ||||
| 
 | ||||
|     // Calculate NcaID
 | ||||
|     // NOTE: Because computing the SHA256 of an entire NCA is quite expensive (especially if the
 | ||||
|     // game is massive), we're going to cheat and only hash the first MB of the NCA.
 | ||||
|     // Also, for XCIs the NcaID matters, so if the override id isn't none, use that.
 | ||||
|     NcaID id{}; | ||||
|     if (override_id == boost::none) { | ||||
|         const auto& data = in->ReadBytes(0x100000); | ||||
|         mbedtls_sha256(data.data(), data.size(), hash.data(), 0); | ||||
|         memcpy(id.data(), hash.data(), 16); | ||||
|     } else { | ||||
|         id = override_id.get(); | ||||
|     } | ||||
| 
 | ||||
|     std::string path = GetRelativePathFromNcaID(id, false, true); | ||||
| 
 | ||||
|     if (GetFileAtID(id) != nullptr && !overwrite_if_exists) { | ||||
|         LOG_WARNING(Loader, "Attempting to overwrite existing NCA. Skipping..."); | ||||
|         return InstallResult::ErrorAlreadyExists; | ||||
|     } | ||||
| 
 | ||||
|     if (GetFileAtID(id) != nullptr) { | ||||
|         LOG_WARNING(Loader, "Overwriting existing NCA..."); | ||||
|         VirtualDir c_dir; | ||||
|         { c_dir = dir->GetFileRelative(path)->GetContainingDirectory(); } | ||||
|         c_dir->DeleteFile(FileUtil::GetFilename(path)); | ||||
|     } | ||||
| 
 | ||||
|     auto out = dir->CreateFileRelative(path); | ||||
|     if (out == nullptr) | ||||
|         return InstallResult::ErrorCopyFailed; | ||||
|     return copy(in, out) ? InstallResult::Success : InstallResult::ErrorCopyFailed; | ||||
| } | ||||
| 
 | ||||
| bool RegisteredCache::RawInstallYuzuMeta(const CNMT& cnmt) { | ||||
|     // Reasoning behind this method can be found in the comment for InstallEntry, NCA overload.
 | ||||
|     const auto dir = this->dir->CreateDirectoryRelative("yuzu_meta"); | ||||
|     const auto filename = GetCNMTName(cnmt.GetType(), cnmt.GetTitleID()); | ||||
|     if (dir->GetFile(filename) == nullptr) { | ||||
|         auto out = dir->CreateFile(filename); | ||||
|         const auto buffer = cnmt.Serialize(); | ||||
|         out->Resize(buffer.size()); | ||||
|         out->WriteBytes(buffer); | ||||
|     } else { | ||||
|         auto out = dir->GetFile(filename); | ||||
|         CNMT old_cnmt(out); | ||||
|         // Returns true on change
 | ||||
|         if (old_cnmt.UnionRecords(cnmt)) { | ||||
|             out->Resize(0); | ||||
|             const auto buffer = old_cnmt.Serialize(); | ||||
|             out->Resize(buffer.size()); | ||||
|             out->WriteBytes(buffer); | ||||
|         } | ||||
|     } | ||||
|     Refresh(); | ||||
|     return std::find_if(yuzu_meta.begin(), yuzu_meta.end(), | ||||
|                         [&cnmt](const std::pair<u64, CNMT>& kv) { | ||||
|                             return kv.second.GetType() == cnmt.GetType() && | ||||
|                                    kv.second.GetTitleID() == cnmt.GetTitleID(); | ||||
|                         }) != yuzu_meta.end(); | ||||
| } | ||||
| } // namespace FileSys
 | ||||
							
								
								
									
										124
									
								
								src/core/file_sys/registered_cache.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								src/core/file_sys/registered_cache.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,124 @@ | ||||
| // Copyright 2018 yuzu emulator team
 | ||||
| // Licensed under GPLv2 or any later version
 | ||||
| // Refer to the license.txt file included.
 | ||||
| 
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include <array> | ||||
| #include <functional> | ||||
| #include <map> | ||||
| #include <memory> | ||||
| #include <string> | ||||
| #include <vector> | ||||
| #include <boost/container/flat_map.hpp> | ||||
| #include "common/common_funcs.h" | ||||
| #include "common/common_types.h" | ||||
| #include "content_archive.h" | ||||
| #include "core/file_sys/nca_metadata.h" | ||||
| #include "core/file_sys/vfs.h" | ||||
| 
 | ||||
| namespace FileSys { | ||||
| class XCI; | ||||
| class CNMT; | ||||
| 
 | ||||
| using NcaID = std::array<u8, 0x10>; | ||||
| using RegisteredCacheParsingFunction = std::function<VirtualFile(const VirtualFile&, const NcaID&)>; | ||||
| using VfsCopyFunction = std::function<bool(VirtualFile, VirtualFile)>; | ||||
| 
 | ||||
| enum class InstallResult { | ||||
|     Success, | ||||
|     ErrorAlreadyExists, | ||||
|     ErrorCopyFailed, | ||||
|     ErrorMetaFailed, | ||||
| }; | ||||
| 
 | ||||
| struct RegisteredCacheEntry { | ||||
|     u64 title_id; | ||||
|     ContentRecordType type; | ||||
| 
 | ||||
|     std::string DebugInfo() const; | ||||
| }; | ||||
| 
 | ||||
| // boost flat_map requires operator< for O(log(n)) lookups.
 | ||||
| bool operator<(const RegisteredCacheEntry& lhs, const RegisteredCacheEntry& rhs); | ||||
| 
 | ||||
| /*
 | ||||
|  * A class that catalogues NCAs in the registered directory structure. | ||||
|  * Nintendo's registered format follows this structure: | ||||
|  * | ||||
|  * Root | ||||
|  *   | 000000XX <- XX is the ____ two digits of the NcaID | ||||
|  *       | <hash>.nca <- hash is the NcaID (first half of SHA256 over entire file) (folder) | ||||
|  *         | 00 | ||||
|  *         | 01 <- Actual content split along 4GB boundaries. (optional) | ||||
|  * | ||||
|  * (This impl also supports substituting the nca dir for an nca file, as that's more convenient when | ||||
|  * 4GB splitting can be ignored.) | ||||
|  */ | ||||
| class RegisteredCache { | ||||
| public: | ||||
|     // Parsing function defines the conversion from raw file to NCA. If there are other steps
 | ||||
|     // besides creating the NCA from the file (e.g. NAX0 on SD Card), that should go in a custom
 | ||||
|     // parsing function.
 | ||||
|     explicit RegisteredCache(VirtualDir dir, | ||||
|                              RegisteredCacheParsingFunction parsing_function = | ||||
|                                  [](const VirtualFile& file, const NcaID& id) { return file; }); | ||||
| 
 | ||||
|     void Refresh(); | ||||
| 
 | ||||
|     bool HasEntry(u64 title_id, ContentRecordType type) const; | ||||
|     bool HasEntry(RegisteredCacheEntry entry) const; | ||||
| 
 | ||||
|     VirtualFile GetEntryRaw(u64 title_id, ContentRecordType type) const; | ||||
|     VirtualFile GetEntryRaw(RegisteredCacheEntry entry) const; | ||||
| 
 | ||||
|     std::shared_ptr<NCA> GetEntry(u64 title_id, ContentRecordType type) const; | ||||
|     std::shared_ptr<NCA> GetEntry(RegisteredCacheEntry entry) const; | ||||
| 
 | ||||
|     std::vector<RegisteredCacheEntry> ListEntries() const; | ||||
|     // If a parameter is not boost::none, it will be filtered for from all entries.
 | ||||
|     std::vector<RegisteredCacheEntry> ListEntriesFilter( | ||||
|         boost::optional<TitleType> title_type = boost::none, | ||||
|         boost::optional<ContentRecordType> record_type = boost::none, | ||||
|         boost::optional<u64> title_id = boost::none) const; | ||||
| 
 | ||||
|     // Raw copies all the ncas from the xci to the csache. Does some quick checks to make sure there
 | ||||
|     // is a meta NCA and all of them are accessible.
 | ||||
|     InstallResult InstallEntry(std::shared_ptr<XCI> xci, bool overwrite_if_exists = false, | ||||
|                                const VfsCopyFunction& copy = &VfsRawCopy); | ||||
| 
 | ||||
|     // Due to the fact that we must use Meta-type NCAs to determine the existance of files, this
 | ||||
|     // poses quite a challenge. Instead of creating a new meta NCA for this file, yuzu will create a
 | ||||
|     // dir inside the NAND called 'yuzu_meta' and store the raw CNMT there.
 | ||||
|     // TODO(DarkLordZach): Author real meta-type NCAs and install those.
 | ||||
|     InstallResult InstallEntry(std::shared_ptr<NCA> nca, TitleType type, | ||||
|                                bool overwrite_if_exists = false, | ||||
|                                const VfsCopyFunction& copy = &VfsRawCopy); | ||||
| 
 | ||||
| private: | ||||
|     template <typename T> | ||||
|     void IterateAllMetadata(std::vector<T>& out, | ||||
|                             std::function<T(const CNMT&, const ContentRecord&)> proc, | ||||
|                             std::function<bool(const CNMT&, const ContentRecord&)> filter) const; | ||||
|     std::vector<NcaID> AccumulateFiles() const; | ||||
|     void ProcessFiles(const std::vector<NcaID>& ids); | ||||
|     void AccumulateYuzuMeta(); | ||||
|     boost::optional<NcaID> GetNcaIDFromMetadata(u64 title_id, ContentRecordType type) const; | ||||
|     VirtualFile GetFileAtID(NcaID id) const; | ||||
|     VirtualFile OpenFileOrDirectoryConcat(const VirtualDir& dir, std::string_view path) const; | ||||
|     InstallResult RawInstallNCA(std::shared_ptr<NCA> nca, const VfsCopyFunction& copy, | ||||
|                                 bool overwrite_if_exists, | ||||
|                                 boost::optional<NcaID> override_id = boost::none); | ||||
|     bool RawInstallYuzuMeta(const CNMT& cnmt); | ||||
| 
 | ||||
|     VirtualDir dir; | ||||
|     RegisteredCacheParsingFunction parser; | ||||
|     // maps tid -> NcaID of meta
 | ||||
|     boost::container::flat_map<u64, NcaID> meta_id; | ||||
|     // maps tid -> meta
 | ||||
|     boost::container::flat_map<u64, CNMT> meta; | ||||
|     // maps tid -> meta for CNMT in yuzu_meta
 | ||||
|     boost::container::flat_map<u64, CNMT> yuzu_meta; | ||||
| }; | ||||
| 
 | ||||
| } // namespace FileSys
 | ||||
| @ -65,7 +65,7 @@ void ProcessFile(VirtualFile file, size_t file_offset, size_t data_offset, u32 t | ||||
|         auto entry = GetEntry<FileEntry>(file, file_offset + this_file_offset); | ||||
| 
 | ||||
|         parent->AddFile(std::make_shared<OffsetVfsFile>( | ||||
|             file, entry.first.size, entry.first.offset + data_offset, entry.second, parent)); | ||||
|             file, entry.first.size, entry.first.offset + data_offset, entry.second)); | ||||
| 
 | ||||
|         if (entry.first.sibling == ROMFS_ENTRY_EMPTY) | ||||
|             break; | ||||
| @ -79,7 +79,7 @@ void ProcessDirectory(VirtualFile file, size_t dir_offset, size_t file_offset, s | ||||
|     while (true) { | ||||
|         auto entry = GetEntry<DirectoryEntry>(file, dir_offset + this_dir_offset); | ||||
|         auto current = std::make_shared<VectorVfsDirectory>( | ||||
|             std::vector<VirtualFile>{}, std::vector<VirtualDir>{}, parent, entry.second); | ||||
|             std::vector<VirtualFile>{}, std::vector<VirtualDir>{}, entry.second); | ||||
| 
 | ||||
|         if (entry.first.child_file != ROMFS_ENTRY_EMPTY) { | ||||
|             ProcessFile(file, file_offset, data_offset, entry.first.child_file, current); | ||||
| @ -108,9 +108,9 @@ VirtualDir ExtractRomFS(VirtualFile file) { | ||||
|     const u64 file_offset = header.file_meta.offset; | ||||
|     const u64 dir_offset = header.directory_meta.offset + 4; | ||||
| 
 | ||||
|     const auto root = | ||||
|     auto root = | ||||
|         std::make_shared<VectorVfsDirectory>(std::vector<VirtualFile>{}, std::vector<VirtualDir>{}, | ||||
|                                              file->GetContainingDirectory(), file->GetName()); | ||||
|                                              file->GetName(), file->GetContainingDirectory()); | ||||
| 
 | ||||
|     ProcessDirectory(file, dir_offset, file_offset, header.data_offset, 0, root); | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										94
									
								
								src/core/file_sys/vfs_concat.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								src/core/file_sys/vfs_concat.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,94 @@ | ||||
| // Copyright 2018 yuzu emulator team
 | ||||
| // Licensed under GPLv2 or any later version
 | ||||
| // Refer to the license.txt file included.
 | ||||
| 
 | ||||
| #include <algorithm> | ||||
| #include <utility> | ||||
| 
 | ||||
| #include "core/file_sys/vfs_concat.h" | ||||
| 
 | ||||
| namespace FileSys { | ||||
| 
 | ||||
| VirtualFile ConcatenateFiles(std::vector<VirtualFile> files, std::string name) { | ||||
|     if (files.empty()) | ||||
|         return nullptr; | ||||
|     if (files.size() == 1) | ||||
|         return files[0]; | ||||
| 
 | ||||
|     return std::shared_ptr<VfsFile>(new ConcatenatedVfsFile(std::move(files), std::move(name))); | ||||
| } | ||||
| 
 | ||||
| ConcatenatedVfsFile::ConcatenatedVfsFile(std::vector<VirtualFile> files_, std::string name) | ||||
|     : name(std::move(name)) { | ||||
|     size_t next_offset = 0; | ||||
|     for (const auto& file : files_) { | ||||
|         files[next_offset] = file; | ||||
|         next_offset += file->GetSize(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| std::string ConcatenatedVfsFile::GetName() const { | ||||
|     if (files.empty()) | ||||
|         return ""; | ||||
|     if (!name.empty()) | ||||
|         return name; | ||||
|     return files.begin()->second->GetName(); | ||||
| } | ||||
| 
 | ||||
| size_t ConcatenatedVfsFile::GetSize() const { | ||||
|     if (files.empty()) | ||||
|         return 0; | ||||
|     return files.rbegin()->first + files.rbegin()->second->GetSize(); | ||||
| } | ||||
| 
 | ||||
| bool ConcatenatedVfsFile::Resize(size_t new_size) { | ||||
|     return false; | ||||
| } | ||||
| 
 | ||||
| std::shared_ptr<VfsDirectory> ConcatenatedVfsFile::GetContainingDirectory() const { | ||||
|     if (files.empty()) | ||||
|         return nullptr; | ||||
|     return files.begin()->second->GetContainingDirectory(); | ||||
| } | ||||
| 
 | ||||
| bool ConcatenatedVfsFile::IsWritable() const { | ||||
|     return false; | ||||
| } | ||||
| 
 | ||||
| bool ConcatenatedVfsFile::IsReadable() const { | ||||
|     return true; | ||||
| } | ||||
| 
 | ||||
| size_t ConcatenatedVfsFile::Read(u8* data, size_t length, size_t offset) const { | ||||
|     auto entry = files.end(); | ||||
|     for (auto iter = files.begin(); iter != files.end(); ++iter) { | ||||
|         if (iter->first > offset) { | ||||
|             entry = --iter; | ||||
|             break; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // Check if the entry should be the last one. The loop above will make it end().
 | ||||
|     if (entry == files.end() && offset < files.rbegin()->first + files.rbegin()->second->GetSize()) | ||||
|         --entry; | ||||
| 
 | ||||
|     if (entry == files.end()) | ||||
|         return 0; | ||||
| 
 | ||||
|     const auto remaining = entry->second->GetSize() + offset - entry->first; | ||||
|     if (length > remaining) { | ||||
|         return entry->second->Read(data, remaining, offset - entry->first) + | ||||
|                Read(data + remaining, length - remaining, offset + remaining); | ||||
|     } | ||||
| 
 | ||||
|     return entry->second->Read(data, length, offset - entry->first); | ||||
| } | ||||
| 
 | ||||
| size_t ConcatenatedVfsFile::Write(const u8* data, size_t length, size_t offset) { | ||||
|     return 0; | ||||
| } | ||||
| 
 | ||||
| bool ConcatenatedVfsFile::Rename(std::string_view name) { | ||||
|     return false; | ||||
| } | ||||
| } // namespace FileSys
 | ||||
							
								
								
									
										41
									
								
								src/core/file_sys/vfs_concat.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/core/file_sys/vfs_concat.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | ||||
| // Copyright 2018 yuzu emulator team
 | ||||
| // Licensed under GPLv2 or any later version
 | ||||
| // Refer to the license.txt file included.
 | ||||
| 
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include <memory> | ||||
| #include <string_view> | ||||
| #include <boost/container/flat_map.hpp> | ||||
| #include "core/file_sys/vfs.h" | ||||
| 
 | ||||
| namespace FileSys { | ||||
| 
 | ||||
| // Wrapper function to allow for more efficient handling of files.size() == 0, 1 cases.
 | ||||
| VirtualFile ConcatenateFiles(std::vector<VirtualFile> files, std::string name = ""); | ||||
| 
 | ||||
| // Class that wraps multiple vfs files and concatenates them, making reads seamless. Currently
 | ||||
| // read-only.
 | ||||
| class ConcatenatedVfsFile : public VfsFile { | ||||
|     friend VirtualFile ConcatenateFiles(std::vector<VirtualFile> files, std::string name); | ||||
| 
 | ||||
|     ConcatenatedVfsFile(std::vector<VirtualFile> files, std::string name); | ||||
| 
 | ||||
| public: | ||||
|     std::string GetName() const override; | ||||
|     size_t GetSize() const override; | ||||
|     bool Resize(size_t new_size) override; | ||||
|     std::shared_ptr<VfsDirectory> GetContainingDirectory() const override; | ||||
|     bool IsWritable() const override; | ||||
|     bool IsReadable() const override; | ||||
|     size_t Read(u8* data, size_t length, size_t offset) const override; | ||||
|     size_t Write(const u8* data, size_t length, size_t offset) override; | ||||
|     bool Rename(std::string_view name) override; | ||||
| 
 | ||||
| private: | ||||
|     // Maps starting offset to file -- more efficient.
 | ||||
|     boost::container::flat_map<u64, VirtualFile> files; | ||||
|     std::string name; | ||||
| }; | ||||
| 
 | ||||
| } // namespace FileSys
 | ||||
| @ -83,8 +83,12 @@ VirtualFile RealVfsFilesystem::OpenFile(std::string_view path_, Mode perms) { | ||||
| 
 | ||||
| VirtualFile RealVfsFilesystem::CreateFile(std::string_view path_, Mode perms) { | ||||
|     const auto path = FileUtil::SanitizePath(path_, FileUtil::DirectorySeparator::PlatformDefault); | ||||
|     if (!FileUtil::Exists(path) && !FileUtil::CreateEmptyFile(path)) | ||||
|         return nullptr; | ||||
|     const auto path_fwd = FileUtil::SanitizePath(path, FileUtil::DirectorySeparator::ForwardSlash); | ||||
|     if (!FileUtil::Exists(path)) { | ||||
|         FileUtil::CreateFullPath(path_fwd); | ||||
|         if (!FileUtil::CreateEmptyFile(path)) | ||||
|             return nullptr; | ||||
|     } | ||||
|     return OpenFile(path, perms); | ||||
| } | ||||
| 
 | ||||
| @ -140,8 +144,12 @@ VirtualDir RealVfsFilesystem::OpenDirectory(std::string_view path_, Mode perms) | ||||
| 
 | ||||
| VirtualDir RealVfsFilesystem::CreateDirectory(std::string_view path_, Mode perms) { | ||||
|     const auto path = FileUtil::SanitizePath(path_, FileUtil::DirectorySeparator::PlatformDefault); | ||||
|     if (!FileUtil::Exists(path) && !FileUtil::CreateDir(path)) | ||||
|         return nullptr; | ||||
|     const auto path_fwd = FileUtil::SanitizePath(path, FileUtil::DirectorySeparator::ForwardSlash); | ||||
|     if (!FileUtil::Exists(path)) { | ||||
|         FileUtil::CreateFullPath(path_fwd); | ||||
|         if (!FileUtil::CreateDir(path)) | ||||
|             return nullptr; | ||||
|     } | ||||
|     // Cannot use make_shared as RealVfsDirectory constructor is private
 | ||||
|     return std::shared_ptr<RealVfsDirectory>(new RealVfsDirectory(*this, path, perms)); | ||||
| } | ||||
| @ -306,14 +314,14 @@ RealVfsDirectory::RealVfsDirectory(RealVfsFilesystem& base_, const std::string& | ||||
| 
 | ||||
| std::shared_ptr<VfsFile> RealVfsDirectory::GetFileRelative(std::string_view path) const { | ||||
|     const auto full_path = FileUtil::SanitizePath(this->path + DIR_SEP + std::string(path)); | ||||
|     if (!FileUtil::Exists(full_path)) | ||||
|     if (!FileUtil::Exists(full_path) || FileUtil::IsDirectory(full_path)) | ||||
|         return nullptr; | ||||
|     return base.OpenFile(full_path, perms); | ||||
| } | ||||
| 
 | ||||
| std::shared_ptr<VfsDirectory> RealVfsDirectory::GetDirectoryRelative(std::string_view path) const { | ||||
|     const auto full_path = FileUtil::SanitizePath(this->path + DIR_SEP + std::string(path)); | ||||
|     if (!FileUtil::Exists(full_path)) | ||||
|     if (!FileUtil::Exists(full_path) || !FileUtil::IsDirectory(full_path)) | ||||
|         return nullptr; | ||||
|     return base.OpenDirectory(full_path, perms); | ||||
| } | ||||
|  | ||||
| @ -5,7 +5,6 @@ | ||||
| #pragma once | ||||
| 
 | ||||
| #include <string_view> | ||||
| 
 | ||||
| #include <boost/container/flat_map.hpp> | ||||
| #include "common/file_util.h" | ||||
| #include "core/file_sys/mode.h" | ||||
|  | ||||
| @ -8,8 +8,8 @@ | ||||
| 
 | ||||
| namespace FileSys { | ||||
| VectorVfsDirectory::VectorVfsDirectory(std::vector<VirtualFile> files_, | ||||
|                                        std::vector<VirtualDir> dirs_, VirtualDir parent_, | ||||
|                                        std::string name_) | ||||
|                                        std::vector<VirtualDir> dirs_, std::string name_, | ||||
|                                        VirtualDir parent_) | ||||
|     : files(std::move(files_)), dirs(std::move(dirs_)), parent(std::move(parent_)), | ||||
|       name(std::move(name_)) {} | ||||
| 
 | ||||
|  | ||||
| @ -13,8 +13,8 @@ namespace FileSys { | ||||
| class VectorVfsDirectory : public VfsDirectory { | ||||
| public: | ||||
|     explicit VectorVfsDirectory(std::vector<VirtualFile> files = {}, | ||||
|                                 std::vector<VirtualDir> dirs = {}, VirtualDir parent = nullptr, | ||||
|                                 std::string name = ""); | ||||
|                                 std::vector<VirtualDir> dirs = {}, std::string name = "", | ||||
|                                 VirtualDir parent = nullptr); | ||||
| 
 | ||||
|     std::vector<std::shared_ptr<VfsFile>> GetFiles() const override; | ||||
|     std::vector<std::shared_ptr<VfsDirectory>> GetSubdirectories() const override; | ||||
|  | ||||
| @ -226,6 +226,7 @@ ResultVal<FileSys::EntryType> VfsDirectoryServiceWrapper::GetEntryType( | ||||
| static std::unique_ptr<FileSys::RomFSFactory> romfs_factory; | ||||
| static std::unique_ptr<FileSys::SaveDataFactory> save_data_factory; | ||||
| static std::unique_ptr<FileSys::SDMCFactory> sdmc_factory; | ||||
| static std::unique_ptr<FileSys::BISFactory> bis_factory; | ||||
| 
 | ||||
| ResultCode RegisterRomFS(std::unique_ptr<FileSys::RomFSFactory>&& factory) { | ||||
|     ASSERT_MSG(romfs_factory == nullptr, "Tried to register a second RomFS"); | ||||
| @ -248,6 +249,13 @@ ResultCode RegisterSDMC(std::unique_ptr<FileSys::SDMCFactory>&& factory) { | ||||
|     return RESULT_SUCCESS; | ||||
| } | ||||
| 
 | ||||
| ResultCode RegisterBIS(std::unique_ptr<FileSys::BISFactory>&& factory) { | ||||
|     ASSERT_MSG(bis_factory == nullptr, "Tried to register a second BIS"); | ||||
|     bis_factory = std::move(factory); | ||||
|     LOG_DEBUG(Service_FS, "Registred BIS"); | ||||
|     return RESULT_SUCCESS; | ||||
| } | ||||
| 
 | ||||
| ResultVal<FileSys::VirtualFile> OpenRomFS(u64 title_id) { | ||||
|     LOG_TRACE(Service_FS, "Opening RomFS for title_id={:016X}", title_id); | ||||
| 
 | ||||
| @ -281,6 +289,14 @@ ResultVal<FileSys::VirtualDir> OpenSDMC() { | ||||
|     return sdmc_factory->Open(); | ||||
| } | ||||
| 
 | ||||
| std::shared_ptr<FileSys::RegisteredCache> GetSystemNANDContents() { | ||||
|     return bis_factory->GetSystemNANDContents(); | ||||
| } | ||||
| 
 | ||||
| std::shared_ptr<FileSys::RegisteredCache> GetUserNANDContents() { | ||||
|     return bis_factory->GetUserNANDContents(); | ||||
| } | ||||
| 
 | ||||
| void RegisterFileSystems(const FileSys::VirtualFilesystem& vfs) { | ||||
|     romfs_factory = nullptr; | ||||
|     save_data_factory = nullptr; | ||||
| @ -291,6 +307,9 @@ void RegisterFileSystems(const FileSys::VirtualFilesystem& vfs) { | ||||
|     auto sd_directory = vfs->OpenDirectory(FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir), | ||||
|                                            FileSys::Mode::ReadWrite); | ||||
| 
 | ||||
|     if (bis_factory == nullptr) | ||||
|         bis_factory = std::make_unique<FileSys::BISFactory>(nand_directory); | ||||
| 
 | ||||
|     auto savedata = std::make_unique<FileSys::SaveDataFactory>(std::move(nand_directory)); | ||||
|     save_data_factory = std::move(savedata); | ||||
| 
 | ||||
|  | ||||
| @ -6,6 +6,7 @@ | ||||
| 
 | ||||
| #include <memory> | ||||
| #include "common/common_types.h" | ||||
| #include "core/file_sys/bis_factory.h" | ||||
| #include "core/file_sys/directory.h" | ||||
| #include "core/file_sys/mode.h" | ||||
| #include "core/file_sys/romfs_factory.h" | ||||
| @ -24,16 +25,15 @@ namespace FileSystem { | ||||
| ResultCode RegisterRomFS(std::unique_ptr<FileSys::RomFSFactory>&& factory); | ||||
| ResultCode RegisterSaveData(std::unique_ptr<FileSys::SaveDataFactory>&& factory); | ||||
| ResultCode RegisterSDMC(std::unique_ptr<FileSys::SDMCFactory>&& factory); | ||||
| ResultCode RegisterBIS(std::unique_ptr<FileSys::BISFactory>&& factory); | ||||
| 
 | ||||
| // TODO(DarkLordZach): BIS Filesystem
 | ||||
| // ResultCode RegisterBIS(std::unique_ptr<FileSys::BISFactory>&& factory);
 | ||||
| ResultVal<FileSys::VirtualFile> OpenRomFS(u64 title_id); | ||||
| ResultVal<FileSys::VirtualDir> OpenSaveData(FileSys::SaveDataSpaceId space, | ||||
|                                             FileSys::SaveDataDescriptor save_struct); | ||||
| ResultVal<FileSys::VirtualDir> OpenSDMC(); | ||||
| 
 | ||||
| // TODO(DarkLordZach): BIS Filesystem
 | ||||
| // ResultVal<std::unique_ptr<FileSys::FileSystemBackend>> OpenBIS();
 | ||||
| std::shared_ptr<FileSys::RegisteredCache> GetSystemNANDContents(); | ||||
| std::shared_ptr<FileSys::RegisteredCache> GetUserNANDContents(); | ||||
| 
 | ||||
| /// Registers all Filesystem services with the specified service manager.
 | ||||
| void InstallInterfaces(SM::ServiceManager& service_manager, const FileSys::VirtualFilesystem& vfs); | ||||
|  | ||||
| @ -41,6 +41,8 @@ FileType IdentifyFile(FileSys::VirtualFile file) { | ||||
| FileType GuessFromFilename(const std::string& name) { | ||||
|     if (name == "main") | ||||
|         return FileType::DeconstructedRomDirectory; | ||||
|     if (name == "00") | ||||
|         return FileType::NCA; | ||||
| 
 | ||||
|     const std::string extension = | ||||
|         Common::ToLower(std::string(FileUtil::GetExtensionFromFilename(name))); | ||||
|  | ||||
| @ -2,6 +2,7 @@ | ||||
| // Licensed under GPLv2 or any later version
 | ||||
| // Refer to the license.txt file included.
 | ||||
| 
 | ||||
| #include <regex> | ||||
| #include <QApplication> | ||||
| #include <QDir> | ||||
| #include <QFileInfo> | ||||
| @ -402,12 +403,72 @@ void GameList::RefreshGameDirectory() { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion) { | ||||
|     boost::container::flat_map<u64, std::shared_ptr<FileSys::NCA>> nca_control_map; | ||||
| static void GetMetadataFromControlNCA(const std::shared_ptr<FileSys::NCA>& nca, | ||||
|                                       std::vector<u8>& icon, std::string& name) { | ||||
|     const auto control_dir = FileSys::ExtractRomFS(nca->GetRomFS()); | ||||
|     if (control_dir == nullptr) | ||||
|         return; | ||||
| 
 | ||||
|     const auto nca_control_callback = | ||||
|         [this, &nca_control_map](u64* num_entries_out, const std::string& directory, | ||||
|                                  const std::string& virtual_name) -> bool { | ||||
|     const auto nacp_file = control_dir->GetFile("control.nacp"); | ||||
|     if (nacp_file == nullptr) | ||||
|         return; | ||||
|     FileSys::NACP nacp(nacp_file); | ||||
|     name = nacp.GetApplicationName(); | ||||
| 
 | ||||
|     FileSys::VirtualFile icon_file = nullptr; | ||||
|     for (const auto& language : FileSys::LANGUAGE_NAMES) { | ||||
|         icon_file = control_dir->GetFile("icon_" + std::string(language) + ".dat"); | ||||
|         if (icon_file != nullptr) { | ||||
|             icon = icon_file->ReadAllBytes(); | ||||
|             break; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void GameListWorker::AddInstalledTitlesToGameList() { | ||||
|     const auto usernand = Service::FileSystem::GetUserNANDContents(); | ||||
|     const auto installed_games = usernand->ListEntriesFilter(FileSys::TitleType::Application, | ||||
|                                                              FileSys::ContentRecordType::Program); | ||||
| 
 | ||||
|     for (const auto& game : installed_games) { | ||||
|         const auto& file = usernand->GetEntryRaw(game); | ||||
|         std::unique_ptr<Loader::AppLoader> loader = Loader::GetLoader(file); | ||||
|         if (!loader) | ||||
|             continue; | ||||
| 
 | ||||
|         std::vector<u8> icon; | ||||
|         std::string name; | ||||
|         u64 program_id; | ||||
|         loader->ReadProgramId(program_id); | ||||
| 
 | ||||
|         const auto& control = | ||||
|             usernand->GetEntry(game.title_id, FileSys::ContentRecordType::Control); | ||||
|         if (control != nullptr) | ||||
|             GetMetadataFromControlNCA(control, icon, name); | ||||
|         emit EntryReady({ | ||||
|             new GameListItemPath( | ||||
|                 FormatGameName(file->GetFullPath()), icon, QString::fromStdString(name), | ||||
|                 QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType())), | ||||
|                 program_id), | ||||
|             new GameListItem( | ||||
|                 QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))), | ||||
|             new GameListItemSize(file->GetSize()), | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     const auto control_data = usernand->ListEntriesFilter(FileSys::TitleType::Application, | ||||
|                                                           FileSys::ContentRecordType::Control); | ||||
| 
 | ||||
|     for (const auto& entry : control_data) { | ||||
|         const auto nca = usernand->GetEntry(entry); | ||||
|         if (nca != nullptr) | ||||
|             nca_control_map.insert_or_assign(entry.title_id, nca); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void GameListWorker::FillControlMap(const std::string& dir_path) { | ||||
|     const auto nca_control_callback = [this](u64* num_entries_out, const std::string& directory, | ||||
|                                              const std::string& virtual_name) -> bool { | ||||
|         std::string physical_name = directory + DIR_SEP + virtual_name; | ||||
| 
 | ||||
|         if (stop_processing) | ||||
| @ -425,10 +486,11 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign | ||||
|     }; | ||||
| 
 | ||||
|     FileUtil::ForeachDirectoryEntry(nullptr, dir_path, nca_control_callback); | ||||
| } | ||||
| 
 | ||||
|     const auto callback = [this, recursion, | ||||
|                            &nca_control_map](u64* num_entries_out, const std::string& directory, | ||||
|                                              const std::string& virtual_name) -> bool { | ||||
| void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion) { | ||||
|     const auto callback = [this, recursion](u64* num_entries_out, const std::string& directory, | ||||
|                                             const std::string& virtual_name) -> bool { | ||||
|         std::string physical_name = directory + DIR_SEP + virtual_name; | ||||
| 
 | ||||
|         if (stop_processing) | ||||
| @ -458,20 +520,7 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign | ||||
|                 // Use from metadata pool.
 | ||||
|                 if (nca_control_map.find(program_id) != nca_control_map.end()) { | ||||
|                     const auto nca = nca_control_map[program_id]; | ||||
|                     const auto control_dir = FileSys::ExtractRomFS(nca->GetRomFS()); | ||||
| 
 | ||||
|                     const auto nacp_file = control_dir->GetFile("control.nacp"); | ||||
|                     FileSys::NACP nacp(nacp_file); | ||||
|                     name = nacp.GetApplicationName(); | ||||
| 
 | ||||
|                     FileSys::VirtualFile icon_file = nullptr; | ||||
|                     for (const auto& language : FileSys::LANGUAGE_NAMES) { | ||||
|                         icon_file = control_dir->GetFile("icon_" + std::string(language) + ".dat"); | ||||
|                         if (icon_file != nullptr) { | ||||
|                             icon = icon_file->ReadAllBytes(); | ||||
|                             break; | ||||
|                         } | ||||
|                     } | ||||
|                     GetMetadataFromControlNCA(nca, icon, name); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
| @ -498,7 +547,10 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign | ||||
| void GameListWorker::run() { | ||||
|     stop_processing = false; | ||||
|     watch_list.append(dir_path); | ||||
|     FillControlMap(dir_path.toStdString()); | ||||
|     AddInstalledTitlesToGameList(); | ||||
|     AddFstEntriesToGameList(dir_path.toStdString(), deep_scan ? 256 : 0); | ||||
|     nca_control_map.clear(); | ||||
|     emit Finished(watch_list); | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -163,10 +163,13 @@ signals: | ||||
| 
 | ||||
| private: | ||||
|     FileSys::VirtualFilesystem vfs; | ||||
|     std::map<u64, std::shared_ptr<FileSys::NCA>> nca_control_map; | ||||
|     QStringList watch_list; | ||||
|     QString dir_path; | ||||
|     bool deep_scan; | ||||
|     std::atomic_bool stop_processing; | ||||
| 
 | ||||
|     void AddInstalledTitlesToGameList(); | ||||
|     void FillControlMap(const std::string& dir_path); | ||||
|     void AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion = 0); | ||||
| }; | ||||
|  | ||||
| @ -27,6 +27,7 @@ | ||||
| #include "common/string_util.h" | ||||
| #include "core/core.h" | ||||
| #include "core/crypto/key_manager.h" | ||||
| #include "core/file_sys/card_image.h" | ||||
| #include "core/file_sys/vfs_real.h" | ||||
| #include "core/gdbstub/gdbstub.h" | ||||
| #include "core/loader/loader.h" | ||||
| @ -117,6 +118,9 @@ GMainWindow::GMainWindow() | ||||
|                        .arg(Common::g_build_name, Common::g_scm_branch, Common::g_scm_desc)); | ||||
|     show(); | ||||
| 
 | ||||
|     // Necessary to load titles from nand in gamelist.
 | ||||
|     Service::FileSystem::RegisterBIS(std::make_unique<FileSys::BISFactory>(vfs->OpenDirectory( | ||||
|         FileUtil::GetUserPath(FileUtil::UserPath::NANDDir), FileSys::Mode::ReadWrite))); | ||||
|     game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan); | ||||
| 
 | ||||
|     // Show one-time "callout" messages to the user
 | ||||
| @ -312,6 +316,8 @@ void GMainWindow::ConnectMenuEvents() { | ||||
|     // File
 | ||||
|     connect(ui.action_Load_File, &QAction::triggered, this, &GMainWindow::OnMenuLoadFile); | ||||
|     connect(ui.action_Load_Folder, &QAction::triggered, this, &GMainWindow::OnMenuLoadFolder); | ||||
|     connect(ui.action_Install_File_NAND, &QAction::triggered, this, | ||||
|             &GMainWindow::OnMenuInstallToNAND); | ||||
|     connect(ui.action_Select_Game_List_Root, &QAction::triggered, this, | ||||
|             &GMainWindow::OnMenuSelectGameListRoot); | ||||
|     connect(ui.action_Exit, &QAction::triggered, this, &QMainWindow::close); | ||||
| @ -615,6 +621,143 @@ void GMainWindow::OnMenuLoadFolder() { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void GMainWindow::OnMenuInstallToNAND() { | ||||
|     const QString file_filter = | ||||
|         tr("Installable Switch File (*.nca *.xci);;Nintendo Content Archive (*.nca);;NX Cartridge " | ||||
|            "Image (*.xci)"); | ||||
|     QString filename = QFileDialog::getOpenFileName(this, tr("Install File"), | ||||
|                                                     UISettings::values.roms_path, file_filter); | ||||
| 
 | ||||
|     const auto qt_raw_copy = [this](FileSys::VirtualFile src, FileSys::VirtualFile dest) { | ||||
|         if (src == nullptr || dest == nullptr) | ||||
|             return false; | ||||
|         if (!dest->Resize(src->GetSize())) | ||||
|             return false; | ||||
| 
 | ||||
|         QProgressDialog progress(fmt::format("Installing file \"{}\"...", src->GetName()).c_str(), | ||||
|                                  "Cancel", 0, src->GetSize() / 0x1000, this); | ||||
|         progress.setWindowModality(Qt::WindowModal); | ||||
| 
 | ||||
|         std::array<u8, 0x1000> buffer{}; | ||||
|         for (size_t i = 0; i < src->GetSize(); i += 0x1000) { | ||||
|             if (progress.wasCanceled()) { | ||||
|                 dest->Resize(0); | ||||
|                 return false; | ||||
|             } | ||||
| 
 | ||||
|             progress.setValue(i / 0x1000); | ||||
|             const auto read = src->Read(buffer.data(), buffer.size(), i); | ||||
|             dest->Write(buffer.data(), read, i); | ||||
|         } | ||||
| 
 | ||||
|         return true; | ||||
|     }; | ||||
| 
 | ||||
|     const auto success = [this]() { | ||||
|         QMessageBox::information(this, tr("Successfully Installed"), | ||||
|                                  tr("The file was successfully installed.")); | ||||
|         game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan); | ||||
|     }; | ||||
| 
 | ||||
|     const auto failed = [this]() { | ||||
|         QMessageBox::warning( | ||||
|             this, tr("Failed to Install"), | ||||
|             tr("There was an error while attempting to install the provided file. It " | ||||
|                "could have an incorrect format or be missing metadata. Please " | ||||
|                "double-check your file and try again.")); | ||||
|     }; | ||||
| 
 | ||||
|     const auto overwrite = [this]() { | ||||
|         return QMessageBox::question(this, "Failed to Install", | ||||
|                                      "The file you are attempting to install already exists " | ||||
|                                      "in the cache. Would you like to overwrite it?") == | ||||
|                QMessageBox::Yes; | ||||
|     }; | ||||
| 
 | ||||
|     if (!filename.isEmpty()) { | ||||
|         if (filename.endsWith("xci", Qt::CaseInsensitive)) { | ||||
|             const auto xci = std::make_shared<FileSys::XCI>( | ||||
|                 vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read)); | ||||
|             if (xci->GetStatus() != Loader::ResultStatus::Success) { | ||||
|                 failed(); | ||||
|                 return; | ||||
|             } | ||||
|             const auto res = | ||||
|                 Service::FileSystem::GetUserNANDContents()->InstallEntry(xci, false, qt_raw_copy); | ||||
|             if (res == FileSys::InstallResult::Success) { | ||||
|                 success(); | ||||
|             } else { | ||||
|                 if (res == FileSys::InstallResult::ErrorAlreadyExists) { | ||||
|                     if (overwrite()) { | ||||
|                         const auto res2 = Service::FileSystem::GetUserNANDContents()->InstallEntry( | ||||
|                             xci, true, qt_raw_copy); | ||||
|                         if (res2 == FileSys::InstallResult::Success) { | ||||
|                             success(); | ||||
|                         } else { | ||||
|                             failed(); | ||||
|                         } | ||||
|                     } | ||||
|                 } else { | ||||
|                     failed(); | ||||
|                 } | ||||
|             } | ||||
|         } else { | ||||
|             const auto nca = std::make_shared<FileSys::NCA>( | ||||
|                 vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read)); | ||||
|             if (nca->GetStatus() != Loader::ResultStatus::Success) { | ||||
|                 failed(); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             static const QStringList tt_options{"System Application", | ||||
|                                                 "System Archive", | ||||
|                                                 "System Application Update", | ||||
|                                                 "Firmware Package (Type A)", | ||||
|                                                 "Firmware Package (Type B)", | ||||
|                                                 "Game", | ||||
|                                                 "Game Update", | ||||
|                                                 "Game DLC", | ||||
|                                                 "Delta Title"}; | ||||
|             bool ok; | ||||
|             const auto item = QInputDialog::getItem( | ||||
|                 this, tr("Select NCA Install Type..."), | ||||
|                 tr("Please select the type of title you would like to install this NCA as:\n(In " | ||||
|                    "most instances, the default 'Game' is fine.)"), | ||||
|                 tt_options, 5, false, &ok); | ||||
| 
 | ||||
|             auto index = tt_options.indexOf(item); | ||||
|             if (!ok || index == -1) { | ||||
|                 QMessageBox::warning(this, tr("Failed to Install"), | ||||
|                                      tr("The title type you selected for the NCA is invalid.")); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             if (index >= 5) | ||||
|                 index += 0x7B; | ||||
| 
 | ||||
|             const auto res = Service::FileSystem::GetUserNANDContents()->InstallEntry( | ||||
|                 nca, static_cast<FileSys::TitleType>(index), false, qt_raw_copy); | ||||
|             if (res == FileSys::InstallResult::Success) { | ||||
|                 success(); | ||||
|             } else { | ||||
|                 if (res == FileSys::InstallResult::ErrorAlreadyExists) { | ||||
|                     if (overwrite()) { | ||||
|                         const auto res2 = Service::FileSystem::GetUserNANDContents()->InstallEntry( | ||||
|                             nca, static_cast<FileSys::TitleType>(index), true, qt_raw_copy); | ||||
|                         if (res2 == FileSys::InstallResult::Success) { | ||||
|                             success(); | ||||
|                         } else { | ||||
|                             failed(); | ||||
|                         } | ||||
|                     } | ||||
|                 } else { | ||||
|                     failed(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void GMainWindow::OnMenuSelectGameListRoot() { | ||||
|     QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory")); | ||||
|     if (!dir_path.isEmpty()) { | ||||
|  | ||||
| @ -125,6 +125,7 @@ private slots: | ||||
|     void OnGameListOpenSaveFolder(u64 program_id); | ||||
|     void OnMenuLoadFile(); | ||||
|     void OnMenuLoadFolder(); | ||||
|     void OnMenuInstallToNAND(); | ||||
|     /// Called whenever a user selects the "File->Select Game List Root" menu item
 | ||||
|     void OnMenuSelectGameListRoot(); | ||||
|     void OnMenuRecentFile(); | ||||
|  | ||||
| @ -57,6 +57,8 @@ | ||||
|       <string>Recent Files</string> | ||||
|      </property> | ||||
|     </widget> | ||||
|      <addaction name="action_Install_File_NAND" /> | ||||
|      <addaction name="separator"/> | ||||
|     <addaction name="action_Load_File"/> | ||||
|     <addaction name="action_Load_Folder"/> | ||||
|     <addaction name="separator"/> | ||||
| @ -102,6 +104,11 @@ | ||||
|    <addaction name="menu_View"/> | ||||
|    <addaction name="menu_Help"/> | ||||
|   </widget> | ||||
|    <action name="action_Install_File_NAND"> | ||||
|      <property name="text"> | ||||
|        <string>Install File to NAND...</string> | ||||
|      </property> | ||||
|    </action> | ||||
|   <action name="action_Load_File"> | ||||
|    <property name="text"> | ||||
|     <string>Load File...</string> | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 bunnei
						bunnei