summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/core/file_sys/registered_cache.cpp435
-rw-r--r--src/core/file_sys/registered_cache.h108
2 files changed, 543 insertions, 0 deletions
diff --git a/src/core/file_sys/registered_cache.cpp b/src/core/file_sys/registered_cache.cpp
new file mode 100644
index 000000000..5440cdefb
--- /dev/null
+++ b/src/core/file_sys/registered_cache.cpp
@@ -0,0 +1,435 @@
1// Copyright 2018 yuzu emulator team
2// Licensed under GPLv2 or any later version
3// Refer to the license.txt file included.
4
5#include <regex>
6#include <mbedtls/sha256.h>
7#include "common/assert.h"
8#include "common/hex_util.h"
9#include "common/logging/log.h"
10#include "core/crypto/encryption_layer.h"
11#include "core/file_sys/card_image.h"
12#include "core/file_sys/nca_metadata.h"
13#include "core/file_sys/registered_cache.h"
14#include "core/file_sys/vfs_concat.h"
15
16namespace FileSys {
17std::string RegisteredCacheEntry::DebugInfo() const {
18 return fmt::format("title_id={:016X}, content_type={:02X}", title_id, static_cast<u8>(type));
19}
20
21bool operator<(const RegisteredCacheEntry& lhs, const RegisteredCacheEntry& rhs) {
22 return (lhs.title_id < rhs.title_id) || (lhs.title_id == rhs.title_id && lhs.type < rhs.type);
23}
24
25static bool FollowsTwoDigitDirFormat(std::string_view name) {
26 const static std::regex two_digit_regex(
27 "000000[0123456789abcdefABCDEF][0123456789abcdefABCDEF]");
28 return std::regex_match(name.begin(), name.end(), two_digit_regex);
29}
30
31static bool FollowsNcaIdFormat(std::string_view name) {
32 const static std::regex nca_id_regex("[0123456789abcdefABCDEF]+.nca");
33 return name.size() == 36 && std::regex_match(name.begin(), name.end(), nca_id_regex);
34}
35
36static std::string GetRelativePathFromNcaID(const std::array<u8, 16>& nca_id, bool second_hex_upper,
37 bool within_two_digit) {
38 if (!within_two_digit)
39 return fmt::format("/{}.nca", HexArrayToString(nca_id, second_hex_upper));
40
41 Core::Crypto::SHA256Hash hash{};
42 mbedtls_sha256(nca_id.data(), nca_id.size(), hash.data(), 0);
43 return fmt::format("/000000{:02X}/{}.nca", hash[0], HexArrayToString(nca_id, second_hex_upper));
44}
45
46static std::string GetCNMTName(TitleType type, u64 title_id) {
47 constexpr std::array<const char*, 9> TITLE_TYPE_NAMES{
48 "SystemProgram",
49 "SystemData",
50 "SystemUpdate",
51 "BootImagePackage",
52 "BootImagePackageSafe",
53 "Application",
54 "Patch",
55 "AddOnContent",
56 "" ///< Currently unknown 'DeltaTitle'
57 };
58
59 size_t index = static_cast<size_t>(type);
60 if (index >= 0x80)
61 index -= 0x80;
62 return fmt::format("{}_{:016x}.cnmt", TITLE_TYPE_NAMES[index], title_id);
63}
64
65static ContentRecordType GetCRTypeFromNCAType(NCAContentType type) {
66 switch (type) {
67 case NCAContentType::Program:
68 // TODO(DarkLordZach): Differentiate between Program and Patch
69 return ContentRecordType::Program;
70 case NCAContentType::Meta:
71 return ContentRecordType::Meta;
72 case NCAContentType::Control:
73 return ContentRecordType::Control;
74 case NCAContentType::Data:
75 return ContentRecordType::Data;
76 case NCAContentType::Manual:
77 // TODO(DarkLordZach): Peek at NCA contents to differentiate Manual and Legal.
78 return ContentRecordType::Manual;
79 default:
80 UNREACHABLE();
81 }
82}
83
84VirtualFile RegisteredCache::OpenFileOrDirectoryConcat(const VirtualDir& dir,
85 std::string_view path) const {
86 if (dir->GetFileRelative(path) != nullptr)
87 return dir->GetFileRelative(path);
88 if (dir->GetDirectoryRelative(path) != nullptr) {
89 const auto nca_dir = dir->GetDirectoryRelative(path);
90 VirtualFile file = nullptr;
91
92 const auto files = nca_dir->GetFiles();
93 if (files.size() == 1 && files[0]->GetName() == "00")
94 file = files[0];
95 else {
96 std::vector<VirtualFile> concat;
97 for (u8 i = 0; i < 0x10; ++i) {
98 auto next = nca_dir->GetFile(fmt::format("{:02X}", i));
99 if (next != nullptr)
100 concat.push_back(std::move(next));
101 else {
102 next = nca_dir->GetFile(fmt::format("{:02x}", i));
103 if (next != nullptr)
104 concat.push_back(std::move(next));
105 else
106 break;
107 }
108 }
109
110 if (concat.empty())
111 return nullptr;
112
113 file = FileSys::ConcatenateFiles(concat);
114 }
115
116 return file;
117 }
118 return nullptr;
119}
120
121VirtualFile RegisteredCache::GetFileAtID(NcaID id) const {
122 VirtualFile file;
123 for (u8 i = 0; i < 4; ++i) {
124 file = OpenFileOrDirectoryConcat(
125 dir, GetRelativePathFromNcaID(id, (i & 0b10) == 0, (i & 0b01) == 0));
126 if (file != nullptr)
127 return file;
128 }
129 return file;
130}
131
132boost::optional<NcaID> RegisteredCache::GetNcaIDFromMetadata(u64 title_id,
133 ContentRecordType type) const {
134 if (type == ContentRecordType::Meta && meta_id.find(title_id) != meta_id.end())
135 return meta_id.at(title_id);
136 if (meta.find(title_id) == meta.end())
137 return boost::none;
138
139 const auto& cnmt = meta.at(title_id);
140
141 const auto iter = std::find_if(cnmt.GetContentRecords().begin(), cnmt.GetContentRecords().end(),
142 [type](const ContentRecord& rec) { return rec.type == type; });
143 if (iter == cnmt.GetContentRecords().end())
144 return boost::none;
145
146 return boost::make_optional(iter->nca_id);
147}
148
149void RegisteredCache::AccumulateFiles(std::vector<NcaID>& ids) const {
150 for (const auto& d2_dir : dir->GetSubdirectories()) {
151 if (FollowsNcaIdFormat(d2_dir->GetName())) {
152 ids.push_back(HexStringToArray<0x10, true>(d2_dir->GetName().substr(0, 0x20)));
153 continue;
154 }
155
156 if (!FollowsTwoDigitDirFormat(d2_dir->GetName()))
157 continue;
158
159 for (const auto& nca_dir : d2_dir->GetSubdirectories()) {
160 if (!FollowsNcaIdFormat(nca_dir->GetName()))
161 continue;
162
163 ids.push_back(HexStringToArray<0x10, true>(nca_dir->GetName().substr(0, 0x20)));
164 }
165
166 for (const auto& nca_file : d2_dir->GetFiles()) {
167 if (!FollowsNcaIdFormat(nca_file->GetName()))
168 continue;
169
170 ids.push_back(HexStringToArray<0x10, true>(nca_file->GetName().substr(0, 0x20)));
171 }
172 }
173
174 for (const auto& d2_file : dir->GetFiles()) {
175 if (FollowsNcaIdFormat(d2_file->GetName()))
176 ids.push_back(HexStringToArray<0x10, true>(d2_file->GetName().substr(0, 0x20)));
177 }
178}
179
180void RegisteredCache::ProcessFiles(const std::vector<NcaID>& ids) {
181 for (const auto& id : ids) {
182 const auto file = GetFileAtID(id);
183
184 if (file == nullptr)
185 continue;
186 const auto nca = std::make_shared<NCA>(parser(file, id));
187 if (nca->GetStatus() != Loader::ResultStatus::Success ||
188 nca->GetType() != NCAContentType::Meta)
189 continue;
190
191 const auto section0 = nca->GetSubdirectories()[0];
192
193 for (const auto& file : section0->GetFiles()) {
194 if (file->GetExtension() != "cnmt")
195 continue;
196
197 meta.insert_or_assign(nca->GetTitleId(), CNMT(file));
198 meta_id.insert_or_assign(nca->GetTitleId(), id);
199 break;
200 }
201 }
202}
203
204void RegisteredCache::AccumulateYuzuMeta() {
205 const auto dir = this->dir->GetSubdirectory("yuzu_meta");
206 if (dir == nullptr)
207 return;
208
209 for (const auto& file : dir->GetFiles()) {
210 if (file->GetExtension() != "cnmt")
211 continue;
212
213 CNMT cnmt(file);
214 yuzu_meta.insert_or_assign(cnmt.GetTitleID(), std::move(cnmt));
215 }
216}
217
218void RegisteredCache::Refresh() {
219 if (dir == nullptr)
220 return;
221 std::vector<NcaID> ids;
222 AccumulateFiles(ids);
223 ProcessFiles(ids);
224 AccumulateYuzuMeta();
225}
226
227RegisteredCache::RegisteredCache(VirtualDir dir_, RegisteredCacheParsingFunction parsing_function)
228 : dir(std::move(dir_)), parser(std::move(parsing_function)) {
229 Refresh();
230}
231
232bool RegisteredCache::HasEntry(u64 title_id, ContentRecordType type) const {
233 return GetEntryRaw(title_id, type) != nullptr;
234}
235
236bool RegisteredCache::HasEntry(RegisteredCacheEntry entry) const {
237 return GetEntryRaw(entry) != nullptr;
238}
239
240VirtualFile RegisteredCache::GetEntryRaw(u64 title_id, ContentRecordType type) const {
241 const auto id = GetNcaIDFromMetadata(title_id, type);
242 if (id == boost::none)
243 return nullptr;
244
245 return parser(GetFileAtID(id.get()), id.get());
246}
247
248VirtualFile RegisteredCache::GetEntryRaw(RegisteredCacheEntry entry) const {
249 return GetEntryRaw(entry.title_id, entry.type);
250}
251
252std::shared_ptr<NCA> RegisteredCache::GetEntry(u64 title_id, ContentRecordType type) const {
253 const auto raw = GetEntryRaw(title_id, type);
254 if (raw == nullptr)
255 return nullptr;
256 return std::make_shared<NCA>(raw);
257}
258
259std::shared_ptr<NCA> RegisteredCache::GetEntry(RegisteredCacheEntry entry) const {
260 return GetEntry(entry.title_id, entry.type);
261}
262
263template <typename T>
264void RegisteredCache::IterateAllMetadata(
265 std::vector<T>& out, std::function<T(const CNMT&, const ContentRecord&)> proc,
266 std::function<bool(const CNMT&, const ContentRecord&)> filter) const {
267 for (const auto& kv : meta) {
268 const auto& cnmt = kv.second;
269 if (filter(cnmt, EMPTY_META_CONTENT_RECORD))
270 out.push_back(proc(cnmt, EMPTY_META_CONTENT_RECORD));
271 for (const auto& rec : cnmt.GetContentRecords()) {
272 if (GetFileAtID(rec.nca_id) != nullptr && filter(cnmt, rec)) {
273 out.push_back(proc(cnmt, rec));
274 }
275 }
276 }
277 for (const auto& kv : yuzu_meta) {
278 const auto& cnmt = kv.second;
279 for (const auto& rec : cnmt.GetContentRecords()) {
280 if (GetFileAtID(rec.nca_id) != nullptr && filter(cnmt, rec)) {
281 out.push_back(proc(cnmt, rec));
282 }
283 }
284 }
285}
286
287std::vector<RegisteredCacheEntry> RegisteredCache::ListEntries() const {
288 std::vector<RegisteredCacheEntry> out;
289 IterateAllMetadata<RegisteredCacheEntry>(
290 out,
291 [](const CNMT& c, const ContentRecord& r) {
292 return RegisteredCacheEntry{c.GetTitleID(), r.type};
293 },
294 [](const CNMT& c, const ContentRecord& r) { return true; });
295 return out;
296}
297
298std::vector<RegisteredCacheEntry> RegisteredCache::ListEntriesFilter(
299 boost::optional<TitleType> title_type, boost::optional<ContentRecordType> record_type,
300 boost::optional<u64> title_id) const {
301 std::vector<RegisteredCacheEntry> out;
302 IterateAllMetadata<RegisteredCacheEntry>(
303 out,
304 [](const CNMT& c, const ContentRecord& r) {
305 return RegisteredCacheEntry{c.GetTitleID(), r.type};
306 },
307 [&title_type, &record_type, &title_id](const CNMT& c, const ContentRecord& r) {
308 if (title_type != boost::none && title_type.get() != c.GetType())
309 return false;
310 if (record_type != boost::none && record_type.get() != r.type)
311 return false;
312 if (title_id != boost::none && title_id.get() != c.GetTitleID())
313 return false;
314 return true;
315 });
316 return out;
317}
318
319static std::shared_ptr<NCA> GetNCAFromXCIForID(std::shared_ptr<XCI> xci, const NcaID& id) {
320 const auto filename = fmt::format("{}.nca", HexArrayToString(id, false));
321 const auto iter =
322 std::find_if(xci->GetNCAs().begin(), xci->GetNCAs().end(),
323 [&filename](std::shared_ptr<NCA> nca) { return nca->GetName() == filename; });
324 return iter == xci->GetNCAs().end() ? nullptr : *iter;
325}
326
327bool RegisteredCache::InstallEntry(std::shared_ptr<XCI> xci) {
328 const auto& ncas = xci->GetNCAs();
329 const auto& meta_iter = std::find_if(ncas.begin(), ncas.end(), [](std::shared_ptr<NCA> nca) {
330 return nca->GetType() == NCAContentType::Meta;
331 });
332
333 if (meta_iter == ncas.end()) {
334 LOG_ERROR(Loader, "The XCI you are attempting to install does not have a metadata NCA and "
335 "is therefore malformed. Double check your encryption keys.");
336 return false;
337 }
338
339 // Install Metadata File
340 const auto meta_id_raw = (*meta_iter)->GetName().substr(0, 32);
341 const auto meta_id = HexStringToArray<16>(meta_id_raw);
342 if (!RawInstallNCA(*meta_iter, meta_id))
343 return false;
344
345 // Install all the other NCAs
346 const auto section0 = (*meta_iter)->GetSubdirectories()[0];
347 const auto cnmt_file = section0->GetFiles()[0];
348 const CNMT cnmt(cnmt_file);
349 for (const auto& record : cnmt.GetContentRecords()) {
350 const auto nca = GetNCAFromXCIForID(xci, record.nca_id);
351 if (nca == nullptr || !RawInstallNCA(nca, record.nca_id))
352 return false;
353 }
354
355 Refresh();
356 return true;
357}
358
359bool RegisteredCache::InstallEntry(std::shared_ptr<NCA> nca, TitleType type) {
360 CNMTHeader header{
361 nca->GetTitleId(), ///< Title ID
362 0, ///< Ignore/Default title version
363 type, ///< Type
364 {}, ///< Padding
365 0x10, ///< Default table offset
366 1, ///< 1 Content Entry
367 0, ///< No Meta Entries
368 {}, ///< Padding
369 };
370 OptionalHeader opt_header{0, 0};
371 ContentRecord c_rec{{}, {}, {}, GetCRTypeFromNCAType(nca->GetType()), {}};
372 const auto& data = nca->GetBaseFile()->ReadBytes(0x100000);
373 mbedtls_sha256(data.data(), data.size(), c_rec.hash.data(), 0);
374 memcpy(&c_rec.nca_id, &c_rec.hash, 16);
375 const CNMT new_cnmt(header, opt_header, {c_rec}, {});
376 return RawInstallYuzuMeta(new_cnmt) && RawInstallNCA(nca, c_rec.nca_id);
377}
378
379bool RegisteredCache::RawInstallNCA(std::shared_ptr<NCA> nca, boost::optional<NcaID> override_id) {
380 const auto in = nca->GetBaseFile();
381 Core::Crypto::SHA256Hash hash{};
382
383 // Calculate NcaID
384 // NOTE: Because computing the SHA256 of an entire NCA is quite expensive (especially if the
385 // game is massive), we're going to cheat and only hash the first MB of the NCA.
386 // Also, for XCIs the NcaID matters, so if the override id isn't none, use that.
387 NcaID id{};
388 if (override_id == boost::none) {
389 const auto& data = in->ReadBytes(0x100000);
390 mbedtls_sha256(data.data(), data.size(), hash.data(), 0);
391 memcpy(id.data(), hash.data(), 16);
392 } else {
393 id = override_id.get();
394 }
395
396 std::string path = GetRelativePathFromNcaID(id, false, true);
397
398 if (GetFileAtID(id) != nullptr) {
399 LOG_WARNING(Loader, "OW Attempt");
400 return false;
401 }
402
403 auto out = dir->CreateFileRelative(path);
404 if (out == nullptr)
405 return false;
406 return VfsRawCopy(in, out);
407}
408
409bool RegisteredCache::RawInstallYuzuMeta(const CNMT& cnmt) {
410 const auto dir = this->dir->CreateDirectoryRelative("yuzu_meta");
411 const auto filename = GetCNMTName(cnmt.GetType(), cnmt.GetTitleID());
412 if (dir->GetFile(filename) == nullptr) {
413 auto out = dir->CreateFile(filename);
414 const auto buffer = cnmt.Serialize();
415 out->Resize(buffer.size());
416 out->WriteBytes(buffer);
417 } else {
418 auto out = dir->GetFile(filename);
419 CNMT old_cnmt(out);
420 // Returns true on change
421 if (old_cnmt.UnionRecords(cnmt)) {
422 out->Resize(0);
423 const auto buffer = old_cnmt.Serialize();
424 out->Resize(buffer.size());
425 out->WriteBytes(buffer);
426 }
427 }
428 Refresh();
429 return std::find_if(yuzu_meta.begin(), yuzu_meta.end(),
430 [&cnmt](const std::pair<const u64, CNMT>& kv) {
431 return kv.second.GetType() == cnmt.GetType() &&
432 kv.second.GetTitleID() == cnmt.GetTitleID();
433 }) != yuzu_meta.end();
434}
435} // namespace FileSys
diff --git a/src/core/file_sys/registered_cache.h b/src/core/file_sys/registered_cache.h
new file mode 100644
index 000000000..ba2e3403f
--- /dev/null
+++ b/src/core/file_sys/registered_cache.h
@@ -0,0 +1,108 @@
1// Copyright 2018 yuzu emulator team
2// Licensed under GPLv2 or any later version
3// Refer to the license.txt file included.
4
5#pragma once
6
7#include <array>
8#include <map>
9#include <memory>
10#include <string>
11#include <boost/container/flat_map.hpp>
12#include "common/common_funcs.h"
13#include "content_archive.h"
14#include "core/file_sys/vfs.h"
15#include "nca_metadata.h"
16
17namespace FileSys {
18class XCI;
19class CNMT;
20
21using NcaID = std::array<u8, 0x10>;
22using RegisteredCacheParsingFunction = std::function<VirtualFile(const VirtualFile&, const NcaID&)>;
23
24struct RegisteredCacheEntry {
25 u64 title_id;
26 ContentRecordType type;
27
28 std::string DebugInfo() const;
29};
30
31// boost flat_map requires operator< for O(log(n)) lookups.
32bool operator<(const RegisteredCacheEntry& lhs, const RegisteredCacheEntry& rhs);
33
34/*
35 * A class that catalogues NCAs in the registered directory structure.
36 * Nintendo's registered format follows this structure:
37 *
38 * Root
39 * | 000000XX <- XX is the ____ two digits of the NcaID
40 * | <hash>.nca <- hash is the NcaID (first half of SHA256 over entire file) (folder)
41 * | 00
42 * | 01 <- Actual content split along 4GB boundaries. (optional)
43 *
44 * (This impl also supports substituting the nca dir for an nca file, as that's more convenient when
45 * 4GB splitting can be ignored.)
46 */
47class RegisteredCache {
48public:
49 // Parsing function defines the conversion from raw file to NCA. If there are other steps
50 // besides creating the NCA from the file (e.g. NAX0 on SD Card), that should go in a custom
51 // parsing function.
52 RegisteredCache(VirtualDir dir,
53 RegisteredCacheParsingFunction parsing_function =
54 [](const VirtualFile& file, const NcaID& id) { return file; });
55
56 void Refresh();
57
58 bool HasEntry(u64 title_id, ContentRecordType type) const;
59 bool HasEntry(RegisteredCacheEntry entry) const;
60
61 VirtualFile GetEntryRaw(u64 title_id, ContentRecordType type) const;
62 VirtualFile GetEntryRaw(RegisteredCacheEntry entry) const;
63
64 std::shared_ptr<NCA> GetEntry(u64 title_id, ContentRecordType type) const;
65 std::shared_ptr<NCA> GetEntry(RegisteredCacheEntry entry) const;
66
67 std::vector<RegisteredCacheEntry> ListEntries() const;
68 // If a parameter is not boost::none, it will be filtered for from all entries.
69 std::vector<RegisteredCacheEntry> ListEntriesFilter(
70 boost::optional<TitleType> title_type = boost::none,
71 boost::optional<ContentRecordType> record_type = boost::none,
72 boost::optional<u64> title_id = boost::none) const;
73
74 // Raw copies all the ncas from the xci to the csache. Does some quick checks to make sure there
75 // is a meta NCA and all of them are accessible.
76 bool InstallEntry(std::shared_ptr<XCI> xci);
77
78 // Due to the fact that we must use Meta-type NCAs to determine the existance of files, this
79 // poses quite a challenge. Instead of creating a new meta NCA for this file, yuzu will create a
80 // dir inside the NAND called 'yuzu_meta' and store the raw CNMT there.
81 // TODO(DarkLordZach): Author real meta-type NCAs and install those.
82 bool InstallEntry(std::shared_ptr<NCA> nca, TitleType type);
83
84private:
85 template <typename T>
86 void IterateAllMetadata(std::vector<T>& out,
87 std::function<T(const CNMT&, const ContentRecord&)> proc,
88 std::function<bool(const CNMT&, const ContentRecord&)> filter) const;
89 void AccumulateFiles(std::vector<NcaID>& ids) const;
90 void ProcessFiles(const std::vector<NcaID>& ids);
91 void AccumulateYuzuMeta();
92 boost::optional<NcaID> GetNcaIDFromMetadata(u64 title_id, ContentRecordType type) const;
93 VirtualFile GetFileAtID(NcaID id) const;
94 VirtualFile OpenFileOrDirectoryConcat(const VirtualDir& dir, std::string_view path) const;
95 bool RawInstallNCA(std::shared_ptr<NCA> nca, boost::optional<NcaID> override_id = boost::none);
96 bool RawInstallYuzuMeta(const CNMT& cnmt);
97
98 VirtualDir dir;
99 RegisteredCacheParsingFunction parser;
100 // maps tid -> NcaID of meta
101 boost::container::flat_map<u64, NcaID> meta_id;
102 // maps tid -> meta
103 boost::container::flat_map<u64, CNMT> meta;
104 // maps tid -> meta for CNMT in yuzu_meta
105 boost::container::flat_map<u64, CNMT> yuzu_meta;
106};
107
108} // namespace FileSys