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 <array>
7 #include <cstddef>
8 #include <cstring>
9 
10 #include "common/file_util.h"
11 #include "common/hex_util.h"
12 #include "common/logging/log.h"
13 #include "common/string_util.h"
14 #include "core/core.h"
15 #include "core/file_sys/common_funcs.h"
16 #include "core/file_sys/content_archive.h"
17 #include "core/file_sys/control_metadata.h"
18 #include "core/file_sys/ips_layer.h"
19 #include "core/file_sys/patch_manager.h"
20 #include "core/file_sys/registered_cache.h"
21 #include "core/file_sys/romfs.h"
22 #include "core/file_sys/vfs_layered.h"
23 #include "core/file_sys/vfs_vector.h"
24 #include "core/hle/service/filesystem/filesystem.h"
25 #include "core/loader/loader.h"
26 #include "core/loader/nso.h"
27 #include "core/memory/cheat_engine.h"
28 #include "core/settings.h"
29 
30 namespace FileSys {
31 namespace {
32 
33 constexpr u32 SINGLE_BYTE_MODULUS = 0x100;
34 
35 constexpr std::array<const char*, 14> EXEFS_FILE_NAMES{
36     "main",    "main.npdm", "rtld",    "sdk",     "subsdk0", "subsdk1", "subsdk2",
37     "subsdk3", "subsdk4",   "subsdk5", "subsdk6", "subsdk7", "subsdk8", "subsdk9",
38 };
39 
40 enum class TitleVersionFormat : u8 {
41     ThreeElements, ///< vX.Y.Z
42     FourElements,  ///< vX.Y.Z.W
43 };
44 
FormatTitleVersion(u32 version,TitleVersionFormat format=TitleVersionFormat::ThreeElements)45 std::string FormatTitleVersion(u32 version,
46                                TitleVersionFormat format = TitleVersionFormat::ThreeElements) {
47     std::array<u8, sizeof(u32)> bytes{};
48     bytes[0] = static_cast<u8>(version % SINGLE_BYTE_MODULUS);
49     for (std::size_t i = 1; i < bytes.size(); ++i) {
50         version /= SINGLE_BYTE_MODULUS;
51         bytes[i] = static_cast<u8>(version % SINGLE_BYTE_MODULUS);
52     }
53 
54     if (format == TitleVersionFormat::FourElements) {
55         return fmt::format("v{}.{}.{}.{}", bytes[3], bytes[2], bytes[1], bytes[0]);
56     }
57     return fmt::format("v{}.{}.{}", bytes[3], bytes[2], bytes[1]);
58 }
59 
60 // Returns a directory with name matching name case-insensitive. Returns nullptr if directory
61 // doesn't have a directory with name.
FindSubdirectoryCaseless(const VirtualDir dir,std::string_view name)62 VirtualDir FindSubdirectoryCaseless(const VirtualDir dir, std::string_view name) {
63 #ifdef _WIN32
64     return dir->GetSubdirectory(name);
65 #else
66     const auto subdirs = dir->GetSubdirectories();
67     for (const auto& subdir : subdirs) {
68         std::string dir_name = Common::ToLower(subdir->GetName());
69         if (dir_name == name) {
70             return subdir;
71         }
72     }
73 
74     return nullptr;
75 #endif
76 }
77 
ReadCheatFileFromFolder(u64 title_id,const PatchManager::BuildID & build_id_,const VirtualDir & base_path,bool upper)78 std::optional<std::vector<Core::Memory::CheatEntry>> ReadCheatFileFromFolder(
79     u64 title_id, const PatchManager::BuildID& build_id_, const VirtualDir& base_path, bool upper) {
80     const auto build_id_raw = Common::HexToString(build_id_, upper);
81     const auto build_id = build_id_raw.substr(0, sizeof(u64) * 2);
82     const auto file = base_path->GetFile(fmt::format("{}.txt", build_id));
83 
84     if (file == nullptr) {
85         LOG_INFO(Common_Filesystem, "No cheats file found for title_id={:016X}, build_id={}",
86                  title_id, build_id);
87         return std::nullopt;
88     }
89 
90     std::vector<u8> data(file->GetSize());
91     if (file->Read(data.data(), data.size()) != data.size()) {
92         LOG_INFO(Common_Filesystem, "Failed to read cheats file for title_id={:016X}, build_id={}",
93                  title_id, build_id);
94         return std::nullopt;
95     }
96 
97     const Core::Memory::TextCheatParser parser;
98     return parser.Parse(std::string_view(reinterpret_cast<const char*>(data.data()), data.size()));
99 }
100 
AppendCommaIfNotEmpty(std::string & to,std::string_view with)101 void AppendCommaIfNotEmpty(std::string& to, std::string_view with) {
102     if (to.empty()) {
103         to += with;
104     } else {
105         to += ", ";
106         to += with;
107     }
108 }
109 
IsDirValidAndNonEmpty(const VirtualDir & dir)110 bool IsDirValidAndNonEmpty(const VirtualDir& dir) {
111     return dir != nullptr && (!dir->GetFiles().empty() || !dir->GetSubdirectories().empty());
112 }
113 } // Anonymous namespace
114 
PatchManager(u64 title_id_,const Service::FileSystem::FileSystemController & fs_controller_,const ContentProvider & content_provider_)115 PatchManager::PatchManager(u64 title_id_,
116                            const Service::FileSystem::FileSystemController& fs_controller_,
117                            const ContentProvider& content_provider_)
118     : title_id{title_id_}, fs_controller{fs_controller_}, content_provider{content_provider_} {}
119 
120 PatchManager::~PatchManager() = default;
121 
GetTitleID() const122 u64 PatchManager::GetTitleID() const {
123     return title_id;
124 }
125 
PatchExeFS(VirtualDir exefs) const126 VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const {
127     LOG_INFO(Loader, "Patching ExeFS for title_id={:016X}", title_id);
128 
129     if (exefs == nullptr)
130         return exefs;
131 
132     if (Settings::values.dump_exefs) {
133         LOG_INFO(Loader, "Dumping ExeFS for title_id={:016X}", title_id);
134         const auto dump_dir = fs_controller.GetModificationDumpRoot(title_id);
135         if (dump_dir != nullptr) {
136             const auto exefs_dir = GetOrCreateDirectoryRelative(dump_dir, "/exefs");
137             VfsRawCopyD(exefs, exefs_dir);
138         }
139     }
140 
141     const auto& disabled = Settings::values.disabled_addons[title_id];
142     const auto update_disabled =
143         std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
144 
145     // Game Updates
146     const auto update_tid = GetUpdateTitleID(title_id);
147     const auto update = content_provider.GetEntry(update_tid, ContentRecordType::Program);
148 
149     if (!update_disabled && update != nullptr && update->GetExeFS() != nullptr &&
150         update->GetStatus() == Loader::ResultStatus::ErrorMissingBKTRBaseRomFS) {
151         LOG_INFO(Loader, "    ExeFS: Update ({}) applied successfully",
152                  FormatTitleVersion(content_provider.GetEntryVersion(update_tid).value_or(0)));
153         exefs = update->GetExeFS();
154     }
155 
156     // LayeredExeFS
157     const auto load_dir = fs_controller.GetModificationLoadRoot(title_id);
158     if (load_dir != nullptr && load_dir->GetSize() > 0) {
159         auto patch_dirs = load_dir->GetSubdirectories();
160         std::sort(
161             patch_dirs.begin(), patch_dirs.end(),
162             [](const VirtualDir& l, const VirtualDir& r) { return l->GetName() < r->GetName(); });
163 
164         std::vector<VirtualDir> layers;
165         layers.reserve(patch_dirs.size() + 1);
166         for (const auto& subdir : patch_dirs) {
167             if (std::find(disabled.begin(), disabled.end(), subdir->GetName()) != disabled.end())
168                 continue;
169 
170             auto exefs_dir = FindSubdirectoryCaseless(subdir, "exefs");
171             if (exefs_dir != nullptr)
172                 layers.push_back(std::move(exefs_dir));
173         }
174         layers.push_back(exefs);
175 
176         auto layered = LayeredVfsDirectory::MakeLayeredDirectory(std::move(layers));
177         if (layered != nullptr) {
178             LOG_INFO(Loader, "    ExeFS: LayeredExeFS patches applied successfully");
179             exefs = std::move(layered);
180         }
181     }
182 
183     return exefs;
184 }
185 
CollectPatches(const std::vector<VirtualDir> & patch_dirs,const std::string & build_id) const186 std::vector<VirtualFile> PatchManager::CollectPatches(const std::vector<VirtualDir>& patch_dirs,
187                                                       const std::string& build_id) const {
188     const auto& disabled = Settings::values.disabled_addons[title_id];
189 
190     std::vector<VirtualFile> out;
191     out.reserve(patch_dirs.size());
192     for (const auto& subdir : patch_dirs) {
193         if (std::find(disabled.cbegin(), disabled.cend(), subdir->GetName()) != disabled.cend())
194             continue;
195 
196         auto exefs_dir = FindSubdirectoryCaseless(subdir, "exefs");
197         if (exefs_dir != nullptr) {
198             for (const auto& file : exefs_dir->GetFiles()) {
199                 if (file->GetExtension() == "ips") {
200                     auto name = file->GetName();
201                     const auto p1 = name.substr(0, name.find('.'));
202                     const auto this_build_id = p1.substr(0, p1.find_last_not_of('0') + 1);
203 
204                     if (build_id == this_build_id)
205                         out.push_back(file);
206                 } else if (file->GetExtension() == "pchtxt") {
207                     IPSwitchCompiler compiler{file};
208                     if (!compiler.IsValid())
209                         continue;
210 
211                     auto this_build_id = Common::HexToString(compiler.GetBuildID());
212                     this_build_id =
213                         this_build_id.substr(0, this_build_id.find_last_not_of('0') + 1);
214 
215                     if (build_id == this_build_id)
216                         out.push_back(file);
217                 }
218             }
219         }
220     }
221 
222     return out;
223 }
224 
PatchNSO(const std::vector<u8> & nso,const std::string & name) const225 std::vector<u8> PatchManager::PatchNSO(const std::vector<u8>& nso, const std::string& name) const {
226     if (nso.size() < sizeof(Loader::NSOHeader)) {
227         return nso;
228     }
229 
230     Loader::NSOHeader header;
231     std::memcpy(&header, nso.data(), sizeof(header));
232 
233     if (header.magic != Common::MakeMagic('N', 'S', 'O', '0')) {
234         return nso;
235     }
236 
237     const auto build_id_raw = Common::HexToString(header.build_id);
238     const auto build_id = build_id_raw.substr(0, build_id_raw.find_last_not_of('0') + 1);
239 
240     if (Settings::values.dump_nso) {
241         LOG_INFO(Loader, "Dumping NSO for name={}, build_id={}, title_id={:016X}", name, build_id,
242                  title_id);
243         const auto dump_dir = fs_controller.GetModificationDumpRoot(title_id);
244         if (dump_dir != nullptr) {
245             const auto nso_dir = GetOrCreateDirectoryRelative(dump_dir, "/nso");
246             const auto file = nso_dir->CreateFile(fmt::format("{}-{}.nso", name, build_id));
247 
248             file->Resize(nso.size());
249             file->WriteBytes(nso);
250         }
251     }
252 
253     LOG_INFO(Loader, "Patching NSO for name={}, build_id={}", name, build_id);
254 
255     const auto load_dir = fs_controller.GetModificationLoadRoot(title_id);
256     if (load_dir == nullptr) {
257         LOG_ERROR(Loader, "Cannot load mods for invalid title_id={:016X}", title_id);
258         return nso;
259     }
260 
261     auto patch_dirs = load_dir->GetSubdirectories();
262     std::sort(patch_dirs.begin(), patch_dirs.end(),
263               [](const VirtualDir& l, const VirtualDir& r) { return l->GetName() < r->GetName(); });
264     const auto patches = CollectPatches(patch_dirs, build_id);
265 
266     auto out = nso;
267     for (const auto& patch_file : patches) {
268         if (patch_file->GetExtension() == "ips") {
269             LOG_INFO(Loader, "    - Applying IPS patch from mod \"{}\"",
270                      patch_file->GetContainingDirectory()->GetParentDirectory()->GetName());
271             const auto patched = PatchIPS(std::make_shared<VectorVfsFile>(out), patch_file);
272             if (patched != nullptr)
273                 out = patched->ReadAllBytes();
274         } else if (patch_file->GetExtension() == "pchtxt") {
275             LOG_INFO(Loader, "    - Applying IPSwitch patch from mod \"{}\"",
276                      patch_file->GetContainingDirectory()->GetParentDirectory()->GetName());
277             const IPSwitchCompiler compiler{patch_file};
278             const auto patched = compiler.Apply(std::make_shared<VectorVfsFile>(out));
279             if (patched != nullptr)
280                 out = patched->ReadAllBytes();
281         }
282     }
283 
284     if (out.size() < sizeof(Loader::NSOHeader)) {
285         return nso;
286     }
287 
288     std::memcpy(out.data(), &header, sizeof(header));
289     return out;
290 }
291 
HasNSOPatch(const BuildID & build_id_) const292 bool PatchManager::HasNSOPatch(const BuildID& build_id_) const {
293     const auto build_id_raw = Common::HexToString(build_id_);
294     const auto build_id = build_id_raw.substr(0, build_id_raw.find_last_not_of('0') + 1);
295 
296     LOG_INFO(Loader, "Querying NSO patch existence for build_id={}", build_id);
297 
298     const auto load_dir = fs_controller.GetModificationLoadRoot(title_id);
299     if (load_dir == nullptr) {
300         LOG_ERROR(Loader, "Cannot load mods for invalid title_id={:016X}", title_id);
301         return false;
302     }
303 
304     auto patch_dirs = load_dir->GetSubdirectories();
305     std::sort(patch_dirs.begin(), patch_dirs.end(),
306               [](const VirtualDir& l, const VirtualDir& r) { return l->GetName() < r->GetName(); });
307 
308     return !CollectPatches(patch_dirs, build_id).empty();
309 }
310 
CreateCheatList(const BuildID & build_id_) const311 std::vector<Core::Memory::CheatEntry> PatchManager::CreateCheatList(
312     const BuildID& build_id_) const {
313     const auto load_dir = fs_controller.GetModificationLoadRoot(title_id);
314     if (load_dir == nullptr) {
315         LOG_ERROR(Loader, "Cannot load mods for invalid title_id={:016X}", title_id);
316         return {};
317     }
318 
319     const auto& disabled = Settings::values.disabled_addons[title_id];
320     auto patch_dirs = load_dir->GetSubdirectories();
321     std::sort(patch_dirs.begin(), patch_dirs.end(),
322               [](const VirtualDir& l, const VirtualDir& r) { return l->GetName() < r->GetName(); });
323 
324     std::vector<Core::Memory::CheatEntry> out;
325     for (const auto& subdir : patch_dirs) {
326         if (std::find(disabled.cbegin(), disabled.cend(), subdir->GetName()) != disabled.cend()) {
327             continue;
328         }
329 
330         auto cheats_dir = FindSubdirectoryCaseless(subdir, "cheats");
331         if (cheats_dir != nullptr) {
332             if (const auto res = ReadCheatFileFromFolder(title_id, build_id_, cheats_dir, true)) {
333                 std::copy(res->begin(), res->end(), std::back_inserter(out));
334                 continue;
335             }
336 
337             if (const auto res = ReadCheatFileFromFolder(title_id, build_id_, cheats_dir, false)) {
338                 std::copy(res->begin(), res->end(), std::back_inserter(out));
339             }
340         }
341     }
342 
343     return out;
344 }
345 
ApplyLayeredFS(VirtualFile & romfs,u64 title_id,ContentRecordType type,const Service::FileSystem::FileSystemController & fs_controller)346 static void ApplyLayeredFS(VirtualFile& romfs, u64 title_id, ContentRecordType type,
347                            const Service::FileSystem::FileSystemController& fs_controller) {
348     const auto load_dir = fs_controller.GetModificationLoadRoot(title_id);
349     if ((type != ContentRecordType::Program && type != ContentRecordType::Data) ||
350         load_dir == nullptr || load_dir->GetSize() <= 0) {
351         return;
352     }
353 
354     auto extracted = ExtractRomFS(romfs);
355     if (extracted == nullptr) {
356         return;
357     }
358 
359     const auto& disabled = Settings::values.disabled_addons[title_id];
360     auto patch_dirs = load_dir->GetSubdirectories();
361     std::sort(patch_dirs.begin(), patch_dirs.end(),
362               [](const VirtualDir& l, const VirtualDir& r) { return l->GetName() < r->GetName(); });
363 
364     std::vector<VirtualDir> layers;
365     std::vector<VirtualDir> layers_ext;
366     layers.reserve(patch_dirs.size() + 1);
367     layers_ext.reserve(patch_dirs.size() + 1);
368     for (const auto& subdir : patch_dirs) {
369         if (std::find(disabled.cbegin(), disabled.cend(), subdir->GetName()) != disabled.cend()) {
370             continue;
371         }
372 
373         auto romfs_dir = FindSubdirectoryCaseless(subdir, "romfs");
374         if (romfs_dir != nullptr)
375             layers.push_back(std::move(romfs_dir));
376 
377         auto ext_dir = FindSubdirectoryCaseless(subdir, "romfs_ext");
378         if (ext_dir != nullptr)
379             layers_ext.push_back(std::move(ext_dir));
380     }
381 
382     // When there are no layers to apply, return early as there is no need to rebuild the RomFS
383     if (layers.empty() && layers_ext.empty()) {
384         return;
385     }
386 
387     layers.push_back(std::move(extracted));
388 
389     auto layered = LayeredVfsDirectory::MakeLayeredDirectory(std::move(layers));
390     if (layered == nullptr) {
391         return;
392     }
393 
394     auto layered_ext = LayeredVfsDirectory::MakeLayeredDirectory(std::move(layers_ext));
395 
396     auto packed = CreateRomFS(std::move(layered), std::move(layered_ext));
397     if (packed == nullptr) {
398         return;
399     }
400 
401     LOG_INFO(Loader, "    RomFS: LayeredFS patches applied successfully");
402     romfs = std::move(packed);
403 }
404 
PatchRomFS(VirtualFile romfs,u64 ivfc_offset,ContentRecordType type,VirtualFile update_raw) const405 VirtualFile PatchManager::PatchRomFS(VirtualFile romfs, u64 ivfc_offset, ContentRecordType type,
406                                      VirtualFile update_raw) const {
407     const auto log_string = fmt::format("Patching RomFS for title_id={:016X}, type={:02X}",
408                                         title_id, static_cast<u8>(type));
409 
410     if (type == ContentRecordType::Program || type == ContentRecordType::Data) {
411         LOG_INFO(Loader, "{}", log_string);
412     } else {
413         LOG_DEBUG(Loader, "{}", log_string);
414     }
415 
416     if (romfs == nullptr) {
417         return romfs;
418     }
419 
420     // Game Updates
421     const auto update_tid = GetUpdateTitleID(title_id);
422     const auto update = content_provider.GetEntryRaw(update_tid, type);
423 
424     const auto& disabled = Settings::values.disabled_addons[title_id];
425     const auto update_disabled =
426         std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
427 
428     if (!update_disabled && update != nullptr) {
429         const auto new_nca = std::make_shared<NCA>(update, romfs, ivfc_offset);
430         if (new_nca->GetStatus() == Loader::ResultStatus::Success &&
431             new_nca->GetRomFS() != nullptr) {
432             LOG_INFO(Loader, "    RomFS: Update ({}) applied successfully",
433                      FormatTitleVersion(content_provider.GetEntryVersion(update_tid).value_or(0)));
434             romfs = new_nca->GetRomFS();
435         }
436     } else if (!update_disabled && update_raw != nullptr) {
437         const auto new_nca = std::make_shared<NCA>(update_raw, romfs, ivfc_offset);
438         if (new_nca->GetStatus() == Loader::ResultStatus::Success &&
439             new_nca->GetRomFS() != nullptr) {
440             LOG_INFO(Loader, "    RomFS: Update (PACKED) applied successfully");
441             romfs = new_nca->GetRomFS();
442         }
443     }
444 
445     // LayeredFS
446     ApplyLayeredFS(romfs, title_id, type, fs_controller);
447 
448     return romfs;
449 }
450 
GetPatchVersionNames(VirtualFile update_raw) const451 PatchManager::PatchVersionNames PatchManager::GetPatchVersionNames(VirtualFile update_raw) const {
452     if (title_id == 0) {
453         return {};
454     }
455 
456     std::map<std::string, std::string, std::less<>> out;
457     const auto& disabled = Settings::values.disabled_addons[title_id];
458 
459     // Game Updates
460     const auto update_tid = GetUpdateTitleID(title_id);
461     PatchManager update{update_tid, fs_controller, content_provider};
462     const auto metadata = update.GetControlMetadata();
463     const auto& nacp = metadata.first;
464 
465     const auto update_disabled =
466         std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
467     const auto update_label = update_disabled ? "[D] Update" : "Update";
468 
469     if (nacp != nullptr) {
470         out.insert_or_assign(update_label, nacp->GetVersionString());
471     } else {
472         if (content_provider.HasEntry(update_tid, ContentRecordType::Program)) {
473             const auto meta_ver = content_provider.GetEntryVersion(update_tid);
474             if (meta_ver.value_or(0) == 0) {
475                 out.insert_or_assign(update_label, "");
476             } else {
477                 out.insert_or_assign(update_label, FormatTitleVersion(*meta_ver));
478             }
479         } else if (update_raw != nullptr) {
480             out.insert_or_assign(update_label, "PACKED");
481         }
482     }
483 
484     // General Mods (LayeredFS and IPS)
485     const auto mod_dir = fs_controller.GetModificationLoadRoot(title_id);
486     if (mod_dir != nullptr && mod_dir->GetSize() > 0) {
487         for (const auto& mod : mod_dir->GetSubdirectories()) {
488             std::string types;
489 
490             const auto exefs_dir = FindSubdirectoryCaseless(mod, "exefs");
491             if (IsDirValidAndNonEmpty(exefs_dir)) {
492                 bool ips = false;
493                 bool ipswitch = false;
494                 bool layeredfs = false;
495 
496                 for (const auto& file : exefs_dir->GetFiles()) {
497                     if (file->GetExtension() == "ips") {
498                         ips = true;
499                     } else if (file->GetExtension() == "pchtxt") {
500                         ipswitch = true;
501                     } else if (std::find(EXEFS_FILE_NAMES.begin(), EXEFS_FILE_NAMES.end(),
502                                          file->GetName()) != EXEFS_FILE_NAMES.end()) {
503                         layeredfs = true;
504                     }
505                 }
506 
507                 if (ips)
508                     AppendCommaIfNotEmpty(types, "IPS");
509                 if (ipswitch)
510                     AppendCommaIfNotEmpty(types, "IPSwitch");
511                 if (layeredfs)
512                     AppendCommaIfNotEmpty(types, "LayeredExeFS");
513             }
514             if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(mod, "romfs")))
515                 AppendCommaIfNotEmpty(types, "LayeredFS");
516             if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(mod, "cheats")))
517                 AppendCommaIfNotEmpty(types, "Cheats");
518 
519             if (types.empty())
520                 continue;
521 
522             const auto mod_disabled =
523                 std::find(disabled.begin(), disabled.end(), mod->GetName()) != disabled.end();
524             out.insert_or_assign(mod_disabled ? "[D] " + mod->GetName() : mod->GetName(), types);
525         }
526     }
527 
528     // DLC
529     const auto dlc_entries =
530         content_provider.ListEntriesFilter(TitleType::AOC, ContentRecordType::Data);
531     std::vector<ContentProviderEntry> dlc_match;
532     dlc_match.reserve(dlc_entries.size());
533     std::copy_if(dlc_entries.begin(), dlc_entries.end(), std::back_inserter(dlc_match),
534                  [this](const ContentProviderEntry& entry) {
535                      return GetBaseTitleID(entry.title_id) == title_id &&
536                             content_provider.GetEntry(entry)->GetStatus() ==
537                                 Loader::ResultStatus::Success;
538                  });
539     if (!dlc_match.empty()) {
540         // Ensure sorted so DLC IDs show in order.
541         std::sort(dlc_match.begin(), dlc_match.end());
542 
543         std::string list;
544         for (size_t i = 0; i < dlc_match.size() - 1; ++i)
545             list += fmt::format("{}, ", dlc_match[i].title_id & 0x7FF);
546 
547         list += fmt::format("{}", dlc_match.back().title_id & 0x7FF);
548 
549         const auto dlc_disabled =
550             std::find(disabled.begin(), disabled.end(), "DLC") != disabled.end();
551         out.insert_or_assign(dlc_disabled ? "[D] DLC" : "DLC", std::move(list));
552     }
553 
554     return out;
555 }
556 
GetGameVersion() const557 std::optional<u32> PatchManager::GetGameVersion() const {
558     const auto update_tid = GetUpdateTitleID(title_id);
559     if (content_provider.HasEntry(update_tid, ContentRecordType::Program)) {
560         return content_provider.GetEntryVersion(update_tid);
561     }
562 
563     return content_provider.GetEntryVersion(title_id);
564 }
565 
GetControlMetadata() const566 PatchManager::Metadata PatchManager::GetControlMetadata() const {
567     const auto base_control_nca = content_provider.GetEntry(title_id, ContentRecordType::Control);
568     if (base_control_nca == nullptr) {
569         return {};
570     }
571 
572     return ParseControlNCA(*base_control_nca);
573 }
574 
ParseControlNCA(const NCA & nca) const575 PatchManager::Metadata PatchManager::ParseControlNCA(const NCA& nca) const {
576     const auto base_romfs = nca.GetRomFS();
577     if (base_romfs == nullptr) {
578         return {};
579     }
580 
581     const auto romfs = PatchRomFS(base_romfs, nca.GetBaseIVFCOffset(), ContentRecordType::Control);
582     if (romfs == nullptr) {
583         return {};
584     }
585 
586     const auto extracted = ExtractRomFS(romfs);
587     if (extracted == nullptr) {
588         return {};
589     }
590 
591     auto nacp_file = extracted->GetFile("control.nacp");
592     if (nacp_file == nullptr) {
593         nacp_file = extracted->GetFile("Control.nacp");
594     }
595 
596     auto nacp = nacp_file == nullptr ? nullptr : std::make_unique<NACP>(nacp_file);
597 
598     VirtualFile icon_file;
599     for (const auto& language : FileSys::LANGUAGE_NAMES) {
600         icon_file = extracted->GetFile(std::string("icon_").append(language).append(".dat"));
601         if (icon_file != nullptr) {
602             break;
603         }
604     }
605 
606     return {std::move(nacp), icon_file};
607 }
608 } // namespace FileSys
609