1 // Copyright 2017 Dolphin Emulator Project
2 // Licensed under GPLv2+
3 // Refer to the license.txt file included.
4
5 #include "Core/WiiUtils.h"
6
7 #include <algorithm>
8 #include <bitset>
9 #include <cinttypes>
10 #include <cstddef>
11 #include <map>
12 #include <memory>
13 #include <optional>
14 #include <sstream>
15 #include <string_view>
16 #include <unordered_set>
17 #include <utility>
18 #include <vector>
19
20 #include <fmt/format.h>
21 #include <pugixml.hpp>
22
23 #include "Common/Assert.h"
24 #include "Common/CommonTypes.h"
25 #include "Common/FileUtil.h"
26 #include "Common/HttpRequest.h"
27 #include "Common/Logging/Log.h"
28 #include "Common/MsgHandler.h"
29 #include "Common/NandPaths.h"
30 #include "Common/StringUtil.h"
31 #include "Common/Swap.h"
32 #include "Core/CommonTitles.h"
33 #include "Core/ConfigManager.h"
34 #include "Core/IOS/Device.h"
35 #include "Core/IOS/ES/ES.h"
36 #include "Core/IOS/ES/Formats.h"
37 #include "Core/IOS/FS/FileSystem.h"
38 #include "Core/IOS/IOS.h"
39 #include "Core/SysConf.h"
40 #include "DiscIO/DiscExtractor.h"
41 #include "DiscIO/Enums.h"
42 #include "DiscIO/Filesystem.h"
43 #include "DiscIO/VolumeDisc.h"
44 #include "DiscIO/VolumeFileBlobReader.h"
45 #include "DiscIO/VolumeWad.h"
46
47 namespace WiiUtils
48 {
ImportWAD(IOS::HLE::Kernel & ios,const DiscIO::VolumeWAD & wad,IOS::HLE::Device::ES::VerifySignature verify_signature)49 static bool ImportWAD(IOS::HLE::Kernel& ios, const DiscIO::VolumeWAD& wad,
50 IOS::HLE::Device::ES::VerifySignature verify_signature)
51 {
52 if (!wad.GetTicket().IsValid() || !wad.GetTMD().IsValid())
53 {
54 PanicAlertT("WAD installation failed: The selected file is not a valid WAD.");
55 return false;
56 }
57
58 const auto tmd = wad.GetTMD();
59 const auto es = ios.GetES();
60
61 IOS::HLE::Device::ES::Context context;
62 IOS::HLE::ReturnCode ret;
63
64 // Ensure the common key index is correct, as it's checked by IOS.
65 IOS::ES::TicketReader ticket = wad.GetTicketWithFixedCommonKey();
66
67 while ((ret = es->ImportTicket(ticket.GetBytes(), wad.GetCertificateChain(),
68 IOS::HLE::Device::ES::TicketImportType::Unpersonalised,
69 verify_signature)) < 0 ||
70 (ret = es->ImportTitleInit(context, tmd.GetBytes(), wad.GetCertificateChain(),
71 verify_signature)) < 0)
72 {
73 if (ret != IOS::HLE::IOSC_FAIL_CHECKVALUE)
74 PanicAlertT("WAD installation failed: Could not initialise title import (error %d).", ret);
75 return false;
76 }
77
78 const bool contents_imported = [&]() {
79 const u64 title_id = tmd.GetTitleId();
80 for (const IOS::ES::Content& content : tmd.GetContents())
81 {
82 const std::vector<u8> data = wad.GetContent(content.index);
83
84 if (es->ImportContentBegin(context, title_id, content.id) < 0 ||
85 es->ImportContentData(context, 0, data.data(), static_cast<u32>(data.size())) < 0 ||
86 es->ImportContentEnd(context, 0) < 0)
87 {
88 PanicAlertT("WAD installation failed: Could not import content %08x.", content.id);
89 return false;
90 }
91 }
92 return true;
93 }();
94
95 if ((contents_imported && es->ImportTitleDone(context) < 0) ||
96 (!contents_imported && es->ImportTitleCancel(context) < 0))
97 {
98 PanicAlertT("WAD installation failed: Could not finalise title import.");
99 return false;
100 }
101
102 return true;
103 }
104
InstallWAD(IOS::HLE::Kernel & ios,const DiscIO::VolumeWAD & wad,InstallType install_type)105 bool InstallWAD(IOS::HLE::Kernel& ios, const DiscIO::VolumeWAD& wad, InstallType install_type)
106 {
107 if (!wad.GetTMD().IsValid())
108 return false;
109
110 SysConf sysconf{ios.GetFS()};
111 SysConf::Entry* tid_entry = sysconf.GetOrAddEntry("IPL.TID", SysConf::Entry::Type::LongLong);
112 const u64 previous_temporary_title_id = Common::swap64(tid_entry->GetData<u64>(0));
113 const u64 title_id = wad.GetTMD().GetTitleId();
114
115 // Skip the install if the WAD is already installed.
116 const auto installed_contents = ios.GetES()->GetStoredContentsFromTMD(wad.GetTMD());
117 if (wad.GetTMD().GetContents() == installed_contents)
118 {
119 // Clear the "temporary title ID" flag in case the user tries to permanently install a title
120 // that has already been imported as a temporary title.
121 if (previous_temporary_title_id == title_id && install_type == InstallType::Permanent)
122 tid_entry->SetData<u64>(0);
123 return true;
124 }
125
126 // If a different version is currently installed, warn the user to make sure
127 // they don't overwrite the current version by mistake.
128 const IOS::ES::TMDReader installed_tmd = ios.GetES()->FindInstalledTMD(title_id);
129 const bool has_another_version =
130 installed_tmd.IsValid() && installed_tmd.GetTitleVersion() != wad.GetTMD().GetTitleVersion();
131 if (has_another_version &&
132 !AskYesNoT("A different version of this title is already installed on the NAND.\n\n"
133 "Installed version: %u\nWAD version: %u\n\n"
134 "Installing this WAD will replace it irreversibly. Continue?",
135 installed_tmd.GetTitleVersion(), wad.GetTMD().GetTitleVersion()))
136 {
137 return false;
138 }
139
140 // Delete a previous temporary title, if it exists.
141 if (previous_temporary_title_id)
142 ios.GetES()->DeleteTitleContent(previous_temporary_title_id);
143
144 // A lot of people use fakesigned WADs, so disable signature checking when installing a WAD.
145 if (!ImportWAD(ios, wad, IOS::HLE::Device::ES::VerifySignature::No))
146 return false;
147
148 // Keep track of the title ID so this title can be removed to make room for any future install.
149 // We use the same mechanism as the System Menu for temporary SD card title data.
150 if (!has_another_version && install_type == InstallType::Temporary)
151 tid_entry->SetData<u64>(Common::swap64(title_id));
152 else
153 tid_entry->SetData<u64>(0);
154
155 return true;
156 }
157
InstallWAD(const std::string & wad_path)158 bool InstallWAD(const std::string& wad_path)
159 {
160 std::unique_ptr<DiscIO::VolumeWAD> wad = DiscIO::CreateWAD(wad_path);
161 if (!wad)
162 return false;
163
164 IOS::HLE::Kernel ios;
165 return InstallWAD(ios, *wad, InstallType::Permanent);
166 }
167
UninstallTitle(u64 title_id)168 bool UninstallTitle(u64 title_id)
169 {
170 IOS::HLE::Kernel ios;
171 return ios.GetES()->DeleteTitleContent(title_id) == IOS::HLE::IPC_SUCCESS;
172 }
173
IsTitleInstalled(u64 title_id)174 bool IsTitleInstalled(u64 title_id)
175 {
176 IOS::HLE::Kernel ios;
177 const auto entries = ios.GetFS()->ReadDirectory(0, 0, Common::GetTitleContentPath(title_id));
178
179 if (!entries)
180 return false;
181
182 // Since this isn't IOS and we only need a simple way to figure out if a title is installed,
183 // we make the (reasonable) assumption that having more than just the TMD in the content
184 // directory means that the title is installed.
185 return std::any_of(entries->begin(), entries->end(),
186 [](const std::string& file) { return file != "title.tmd"; });
187 }
188
189 // Common functionality for system updaters.
190 class SystemUpdater
191 {
192 public:
193 virtual ~SystemUpdater() = default;
194
195 protected:
196 struct TitleInfo
197 {
198 u64 id;
199 u16 version;
200 };
201
202 std::string GetDeviceRegion();
203 std::string GetDeviceId();
204
205 IOS::HLE::Kernel m_ios;
206 };
207
GetDeviceRegion()208 std::string SystemUpdater::GetDeviceRegion()
209 {
210 // Try to determine the region from an installed system menu.
211 const auto tmd = m_ios.GetES()->FindInstalledTMD(Titles::SYSTEM_MENU);
212 if (tmd.IsValid())
213 {
214 const DiscIO::Region region = tmd.GetRegion();
215 static const std::map<DiscIO::Region, std::string> regions = {{DiscIO::Region::NTSC_J, "JPN"},
216 {DiscIO::Region::NTSC_U, "USA"},
217 {DiscIO::Region::PAL, "EUR"},
218 {DiscIO::Region::NTSC_K, "KOR"},
219 {DiscIO::Region::Unknown, "EUR"}};
220 return regions.at(region);
221 }
222 return "";
223 }
224
GetDeviceId()225 std::string SystemUpdater::GetDeviceId()
226 {
227 u32 ios_device_id;
228 if (m_ios.GetES()->GetDeviceId(&ios_device_id) < 0)
229 return "";
230 return std::to_string((u64(1) << 32) | ios_device_id);
231 }
232
233 class OnlineSystemUpdater final : public SystemUpdater
234 {
235 public:
236 OnlineSystemUpdater(UpdateCallback update_callback, const std::string& region);
237 UpdateResult DoOnlineUpdate();
238
239 private:
240 struct Response
241 {
242 std::string content_prefix_url;
243 std::vector<TitleInfo> titles;
244 };
245
246 Response GetSystemTitles();
247 Response ParseTitlesResponse(const std::vector<u8>& response) const;
248 bool ShouldInstallTitle(const TitleInfo& title);
249
250 UpdateResult InstallTitleFromNUS(const std::string& prefix_url, const TitleInfo& title,
251 std::unordered_set<u64>* updated_titles);
252
253 // Helper functions to download contents from NUS.
254 std::pair<IOS::ES::TMDReader, std::vector<u8>> DownloadTMD(const std::string& prefix_url,
255 const TitleInfo& title);
256 std::pair<std::vector<u8>, std::vector<u8>> DownloadTicket(const std::string& prefix_url,
257 const TitleInfo& title);
258 std::optional<std::vector<u8>> DownloadContent(const std::string& prefix_url,
259 const TitleInfo& title, u32 cid);
260
261 UpdateCallback m_update_callback;
262 std::string m_requested_region;
263 Common::HttpRequest m_http{std::chrono::minutes{3}};
264 };
265
OnlineSystemUpdater(UpdateCallback update_callback,const std::string & region)266 OnlineSystemUpdater::OnlineSystemUpdater(UpdateCallback update_callback, const std::string& region)
267 : m_update_callback(std::move(update_callback)), m_requested_region(region)
268 {
269 }
270
271 OnlineSystemUpdater::Response
ParseTitlesResponse(const std::vector<u8> & response) const272 OnlineSystemUpdater::ParseTitlesResponse(const std::vector<u8>& response) const
273 {
274 pugi::xml_document doc;
275 pugi::xml_parse_result result = doc.load_buffer(response.data(), response.size());
276 if (!result)
277 {
278 ERROR_LOG(CORE, "ParseTitlesResponse: Could not parse response");
279 return {};
280 }
281
282 // pugixml doesn't fully support namespaces and ignores them.
283 const pugi::xml_node node = doc.select_node("//GetSystemUpdateResponse").node();
284 if (!node)
285 {
286 ERROR_LOG(CORE, "ParseTitlesResponse: Could not find response node");
287 return {};
288 }
289
290 const int code = node.child("ErrorCode").text().as_int();
291 if (code != 0)
292 {
293 ERROR_LOG(CORE, "ParseTitlesResponse: Non-zero error code (%d)", code);
294 return {};
295 }
296
297 // libnup uses the uncached URL, not the cached one. However, that one is way, way too slow,
298 // so let's use the cached endpoint.
299 Response info;
300 info.content_prefix_url = node.child("ContentPrefixURL").text().as_string();
301 // Disable HTTPS because we can't use it without a device certificate.
302 info.content_prefix_url = ReplaceAll(info.content_prefix_url, "https://", "http://");
303 if (info.content_prefix_url.empty())
304 {
305 ERROR_LOG(CORE, "ParseTitlesResponse: Empty content prefix URL");
306 return {};
307 }
308
309 for (const pugi::xml_node& title_node : node.children("TitleVersion"))
310 {
311 const u64 title_id = std::stoull(title_node.child("TitleId").text().as_string(), nullptr, 16);
312 const u16 title_version = static_cast<u16>(title_node.child("Version").text().as_uint());
313 info.titles.push_back({title_id, title_version});
314 }
315 return info;
316 }
317
ShouldInstallTitle(const TitleInfo & title)318 bool OnlineSystemUpdater::ShouldInstallTitle(const TitleInfo& title)
319 {
320 const auto es = m_ios.GetES();
321 const auto installed_tmd = es->FindInstalledTMD(title.id);
322 return !(installed_tmd.IsValid() && installed_tmd.GetTitleVersion() >= title.version &&
323 es->GetStoredContentsFromTMD(installed_tmd).size() == installed_tmd.GetNumContents());
324 }
325
326 constexpr const char* GET_SYSTEM_TITLES_REQUEST_PAYLOAD = R"(<?xml version="1.0" encoding="UTF-8"?>
327 <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
328 xmlns:xsd="http://www.w3.org/2001/XMLSchema"
329 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
330 <soapenv:Body>
331 <GetSystemUpdateRequest xmlns="urn:nus.wsapi.broadon.com">
332 <Version>1.0</Version>
333 <MessageId>0</MessageId>
334 <DeviceId></DeviceId>
335 <RegionId></RegionId>
336 </GetSystemUpdateRequest>
337 </soapenv:Body>
338 </soapenv:Envelope>
339 )";
340
GetSystemTitles()341 OnlineSystemUpdater::Response OnlineSystemUpdater::GetSystemTitles()
342 {
343 // Construct the request by loading the template first, then updating some fields.
344 pugi::xml_document doc;
345 pugi::xml_parse_result result = doc.load_string(GET_SYSTEM_TITLES_REQUEST_PAYLOAD);
346 ASSERT(result);
347
348 // Nintendo does not really care about the device ID or verify that we *are* that device,
349 // as long as it is a valid Wii device ID.
350 const std::string device_id = GetDeviceId();
351 ASSERT(doc.select_node("//DeviceId").node().text().set(device_id.c_str()));
352
353 // Write the correct device region.
354 const std::string region = m_requested_region.empty() ? GetDeviceRegion() : m_requested_region;
355 ASSERT(doc.select_node("//RegionId").node().text().set(region.c_str()));
356
357 std::ostringstream stream;
358 doc.save(stream);
359 const std::string request = stream.str();
360
361 // Note: We don't use HTTPS because that would require the user to have
362 // a device certificate which cannot be redistributed with Dolphin.
363 // This is fine, because IOS has signature checks.
364 const Common::HttpRequest::Response response =
365 m_http.Post("http://nus.shop.wii.com/nus/services/NetUpdateSOAP", request,
366 {
367 {"SOAPAction", "urn:nus.wsapi.broadon.com/GetSystemUpdate"},
368 {"User-Agent", "wii libnup/1.0"},
369 {"Content-Type", "text/xml; charset=utf-8"},
370 });
371
372 if (!response)
373 return {};
374 return ParseTitlesResponse(*response);
375 }
376
DoOnlineUpdate()377 UpdateResult OnlineSystemUpdater::DoOnlineUpdate()
378 {
379 const Response info = GetSystemTitles();
380 if (info.titles.empty())
381 return UpdateResult::ServerFailed;
382
383 // Download and install any title that is older than the NUS version.
384 // The order is determined by the server response, which is: boot2, System Menu, IOSes, channels.
385 // As we install any IOS required by titles, the real order is boot2, SM IOS, SM, IOSes, channels.
386 std::unordered_set<u64> updated_titles;
387 size_t processed = 0;
388 for (const TitleInfo& title : info.titles)
389 {
390 if (!m_update_callback(processed++, info.titles.size(), title.id))
391 return UpdateResult::Cancelled;
392
393 const UpdateResult res = InstallTitleFromNUS(info.content_prefix_url, title, &updated_titles);
394 if (res != UpdateResult::Succeeded)
395 {
396 ERROR_LOG(CORE, "Failed to update %016" PRIx64 " -- aborting update", title.id);
397 return res;
398 }
399
400 m_update_callback(processed, info.titles.size(), title.id);
401 }
402
403 if (updated_titles.empty())
404 {
405 NOTICE_LOG(CORE, "Update finished - Already up-to-date");
406 return UpdateResult::AlreadyUpToDate;
407 }
408 NOTICE_LOG(CORE, "Update finished - %zu updates installed", updated_titles.size());
409 return UpdateResult::Succeeded;
410 }
411
InstallTitleFromNUS(const std::string & prefix_url,const TitleInfo & title,std::unordered_set<u64> * updated_titles)412 UpdateResult OnlineSystemUpdater::InstallTitleFromNUS(const std::string& prefix_url,
413 const TitleInfo& title,
414 std::unordered_set<u64>* updated_titles)
415 {
416 // We currently don't support boot2 updates at all, so ignore any attempt to install it.
417 if (title.id == Titles::BOOT2)
418 return UpdateResult::Succeeded;
419
420 if (!ShouldInstallTitle(title) || updated_titles->find(title.id) != updated_titles->end())
421 return UpdateResult::Succeeded;
422
423 NOTICE_LOG(CORE, "Updating title %016" PRIx64, title.id);
424
425 // Download the ticket and certificates.
426 const auto ticket = DownloadTicket(prefix_url, title);
427 if (ticket.first.empty() || ticket.second.empty())
428 {
429 ERROR_LOG(CORE, "Failed to download ticket and certs");
430 return UpdateResult::DownloadFailed;
431 }
432
433 // Import the ticket.
434 IOS::HLE::ReturnCode ret = IOS::HLE::IPC_SUCCESS;
435 const auto es = m_ios.GetES();
436 if ((ret = es->ImportTicket(ticket.first, ticket.second)) < 0)
437 {
438 ERROR_LOG(CORE, "Failed to import ticket: error %d", ret);
439 return UpdateResult::ImportFailed;
440 }
441
442 // Download the TMD.
443 const auto tmd = DownloadTMD(prefix_url, title);
444 if (!tmd.first.IsValid())
445 {
446 ERROR_LOG(CORE, "Failed to download TMD");
447 return UpdateResult::DownloadFailed;
448 }
449
450 // Download and import any required system title first.
451 const u64 ios_id = tmd.first.GetIOSId();
452 if (ios_id != 0 && IOS::ES::IsTitleType(ios_id, IOS::ES::TitleType::System))
453 {
454 if (!es->FindInstalledTMD(ios_id).IsValid())
455 {
456 WARN_LOG(CORE, "Importing required system title %016" PRIx64 " first", ios_id);
457 const UpdateResult res = InstallTitleFromNUS(prefix_url, {ios_id, 0}, updated_titles);
458 if (res != UpdateResult::Succeeded)
459 {
460 ERROR_LOG(CORE, "Failed to import required system title %016" PRIx64, ios_id);
461 return res;
462 }
463 }
464 }
465
466 // Initialise the title import.
467 IOS::HLE::Device::ES::Context context;
468 if ((ret = es->ImportTitleInit(context, tmd.first.GetBytes(), tmd.second)) < 0)
469 {
470 ERROR_LOG(CORE, "Failed to initialise title import: error %d", ret);
471 return UpdateResult::ImportFailed;
472 }
473
474 // Now download and install contents listed in the TMD.
475 const std::vector<IOS::ES::Content> stored_contents = es->GetStoredContentsFromTMD(tmd.first);
476 const UpdateResult import_result = [&]() {
477 for (const IOS::ES::Content& content : tmd.first.GetContents())
478 {
479 const bool is_already_installed = std::find_if(stored_contents.begin(), stored_contents.end(),
480 [&content](const auto& stored_content) {
481 return stored_content.id == content.id;
482 }) != stored_contents.end();
483
484 // Do skip what is already installed on the NAND.
485 if (is_already_installed)
486 continue;
487
488 if ((ret = es->ImportContentBegin(context, title.id, content.id)) < 0)
489 {
490 ERROR_LOG(CORE, "Failed to initialise import for content %08x: error %d", content.id, ret);
491 return UpdateResult::ImportFailed;
492 }
493
494 const std::optional<std::vector<u8>> data = DownloadContent(prefix_url, title, content.id);
495 if (!data)
496 {
497 ERROR_LOG(CORE, "Failed to download content %08x", content.id);
498 return UpdateResult::DownloadFailed;
499 }
500
501 if (es->ImportContentData(context, 0, data->data(), static_cast<u32>(data->size())) < 0 ||
502 es->ImportContentEnd(context, 0) < 0)
503 {
504 ERROR_LOG(CORE, "Failed to import content %08x", content.id);
505 return UpdateResult::ImportFailed;
506 }
507 }
508 return UpdateResult::Succeeded;
509 }();
510 const bool all_contents_imported = import_result == UpdateResult::Succeeded;
511
512 if ((all_contents_imported && (ret = es->ImportTitleDone(context)) < 0) ||
513 (!all_contents_imported && (ret = es->ImportTitleCancel(context)) < 0))
514 {
515 ERROR_LOG(CORE, "Failed to finalise title import: error %d", ret);
516 return UpdateResult::ImportFailed;
517 }
518
519 if (!all_contents_imported)
520 return import_result;
521
522 updated_titles->emplace(title.id);
523 return UpdateResult::Succeeded;
524 }
525
526 std::pair<IOS::ES::TMDReader, std::vector<u8>>
DownloadTMD(const std::string & prefix_url,const TitleInfo & title)527 OnlineSystemUpdater::DownloadTMD(const std::string& prefix_url, const TitleInfo& title)
528 {
529 const std::string url = (title.version == 0) ?
530 fmt::format("{}/{:016x}/tmd", prefix_url, title.id) :
531 fmt::format("{}/{:016x}/tmd.{}", prefix_url, title.id, title.version);
532 const Common::HttpRequest::Response response = m_http.Get(url);
533 if (!response)
534 return {};
535
536 // Too small to contain both the TMD and a cert chain.
537 if (response->size() <= sizeof(IOS::ES::TMDHeader))
538 return {};
539 const size_t tmd_size =
540 sizeof(IOS::ES::TMDHeader) +
541 sizeof(IOS::ES::Content) *
542 Common::swap16(response->data() + offsetof(IOS::ES::TMDHeader, num_contents));
543 if (response->size() <= tmd_size)
544 return {};
545
546 const auto tmd_begin = response->begin();
547 const auto tmd_end = tmd_begin + tmd_size;
548
549 return {IOS::ES::TMDReader(std::vector<u8>(tmd_begin, tmd_end)),
550 std::vector<u8>(tmd_end, response->end())};
551 }
552
553 std::pair<std::vector<u8>, std::vector<u8>>
DownloadTicket(const std::string & prefix_url,const TitleInfo & title)554 OnlineSystemUpdater::DownloadTicket(const std::string& prefix_url, const TitleInfo& title)
555 {
556 const std::string url = fmt::format("{}/{:016x}/cetk", prefix_url, title.id);
557 const Common::HttpRequest::Response response = m_http.Get(url);
558 if (!response)
559 return {};
560
561 // Too small to contain both the ticket and a cert chain.
562 if (response->size() <= sizeof(IOS::ES::Ticket))
563 return {};
564
565 const auto ticket_begin = response->begin();
566 const auto ticket_end = ticket_begin + sizeof(IOS::ES::Ticket);
567 return {std::vector<u8>(ticket_begin, ticket_end), std::vector<u8>(ticket_end, response->end())};
568 }
569
DownloadContent(const std::string & prefix_url,const TitleInfo & title,u32 cid)570 std::optional<std::vector<u8>> OnlineSystemUpdater::DownloadContent(const std::string& prefix_url,
571 const TitleInfo& title, u32 cid)
572 {
573 const std::string url = fmt::format("{}/{:016x}/{:08x}", prefix_url, title.id, cid);
574 return m_http.Get(url);
575 }
576
577 class DiscSystemUpdater final : public SystemUpdater
578 {
579 public:
DiscSystemUpdater(UpdateCallback update_callback,const std::string & image_path)580 DiscSystemUpdater(UpdateCallback update_callback, const std::string& image_path)
581 : m_update_callback{std::move(update_callback)}, m_volume{DiscIO::CreateDisc(image_path)}
582 {
583 }
584 UpdateResult DoDiscUpdate();
585
586 private:
587 #pragma pack(push, 1)
588 struct ManifestHeader
589 {
590 char timestamp[0x10]; // YYYY/MM/DD
591 // There is a u32 in newer info files to indicate the number of entries,
592 // but it's not used in older files, and it's not always at the same offset.
593 // Too unreliable to use it.
594 u32 padding[4];
595 };
596 static_assert(sizeof(ManifestHeader) == 32, "Wrong size");
597
598 struct Entry
599 {
600 u32 type;
601 u32 attribute;
602 u32 unknown1;
603 u32 unknown2;
604 char path[0x40];
605 u64 title_id;
606 u16 title_version;
607 u16 unused1[3];
608 char name[0x40];
609 char info[0x40];
610 u8 unused2[0x120];
611 };
612 static_assert(sizeof(Entry) == 512, "Wrong size");
613 #pragma pack(pop)
614
615 UpdateResult UpdateFromManifest(std::string_view manifest_name);
616 UpdateResult ProcessEntry(u32 type, std::bitset<32> attrs, const TitleInfo& title,
617 std::string_view path);
618
619 UpdateCallback m_update_callback;
620 std::unique_ptr<DiscIO::VolumeDisc> m_volume;
621 DiscIO::Partition m_partition;
622 };
623
DoDiscUpdate()624 UpdateResult DiscSystemUpdater::DoDiscUpdate()
625 {
626 if (!m_volume)
627 return UpdateResult::DiscReadFailed;
628
629 // Do not allow mismatched regions, because installing an update will automatically change
630 // the Wii's region and may result in semi/full system menu bricks.
631 const IOS::ES::TMDReader system_menu_tmd = m_ios.GetES()->FindInstalledTMD(Titles::SYSTEM_MENU);
632 if (system_menu_tmd.IsValid() && m_volume->GetRegion() != system_menu_tmd.GetRegion())
633 return UpdateResult::RegionMismatch;
634
635 const auto partitions = m_volume->GetPartitions();
636 const auto update_partition =
637 std::find_if(partitions.cbegin(), partitions.cend(), [&](const DiscIO::Partition& partition) {
638 return m_volume->GetPartitionType(partition) == 1u;
639 });
640
641 if (update_partition == partitions.cend())
642 {
643 ERROR_LOG(CORE, "Could not find any update partition");
644 return UpdateResult::MissingUpdatePartition;
645 }
646
647 m_partition = *update_partition;
648
649 return UpdateFromManifest("__update.inf");
650 }
651
UpdateFromManifest(std::string_view manifest_name)652 UpdateResult DiscSystemUpdater::UpdateFromManifest(std::string_view manifest_name)
653 {
654 const DiscIO::FileSystem* disc_fs = m_volume->GetFileSystem(m_partition);
655 if (!disc_fs)
656 {
657 ERROR_LOG(CORE, "Could not read the update partition file system");
658 return UpdateResult::DiscReadFailed;
659 }
660
661 const std::unique_ptr<DiscIO::FileInfo> update_manifest = disc_fs->FindFileInfo(manifest_name);
662 if (!update_manifest ||
663 (update_manifest->GetSize() - sizeof(ManifestHeader)) % sizeof(Entry) != 0)
664 {
665 ERROR_LOG(CORE, "Invalid or missing update manifest");
666 return UpdateResult::DiscReadFailed;
667 }
668
669 const u32 num_entries = (update_manifest->GetSize() - sizeof(ManifestHeader)) / sizeof(Entry);
670 if (num_entries > 200)
671 return UpdateResult::DiscReadFailed;
672
673 std::vector<u8> entry(sizeof(Entry));
674 size_t updates_installed = 0;
675 for (u32 i = 0; i < num_entries; ++i)
676 {
677 const u32 offset = sizeof(ManifestHeader) + sizeof(Entry) * i;
678 if (entry.size() != DiscIO::ReadFile(*m_volume, m_partition, update_manifest.get(),
679 entry.data(), entry.size(), offset))
680 {
681 ERROR_LOG(CORE, "Failed to read update information from update manifest");
682 return UpdateResult::DiscReadFailed;
683 }
684
685 const u32 type = Common::swap32(entry.data() + offsetof(Entry, type));
686 const std::bitset<32> attrs = Common::swap32(entry.data() + offsetof(Entry, attribute));
687 const u64 title_id = Common::swap64(entry.data() + offsetof(Entry, title_id));
688 const u16 title_version = Common::swap16(entry.data() + offsetof(Entry, title_version));
689 const char* path_pointer = reinterpret_cast<const char*>(entry.data() + offsetof(Entry, path));
690 const std::string_view path{path_pointer, strnlen(path_pointer, sizeof(Entry::path))};
691
692 if (!m_update_callback(i, num_entries, title_id))
693 return UpdateResult::Cancelled;
694
695 const UpdateResult res = ProcessEntry(type, attrs, {title_id, title_version}, path);
696 if (res != UpdateResult::Succeeded && res != UpdateResult::AlreadyUpToDate)
697 {
698 ERROR_LOG(CORE, "Failed to update %016" PRIx64 " -- aborting update", title_id);
699 return res;
700 }
701
702 if (res == UpdateResult::Succeeded)
703 ++updates_installed;
704 }
705 return updates_installed == 0 ? UpdateResult::AlreadyUpToDate : UpdateResult::Succeeded;
706 }
707
ProcessEntry(u32 type,std::bitset<32> attrs,const TitleInfo & title,std::string_view path)708 UpdateResult DiscSystemUpdater::ProcessEntry(u32 type, std::bitset<32> attrs,
709 const TitleInfo& title, std::string_view path)
710 {
711 // Skip any unknown type and boot2 updates (for now).
712 if (type != 2 && type != 3 && type != 6 && type != 7)
713 return UpdateResult::AlreadyUpToDate;
714
715 const IOS::ES::TMDReader tmd = m_ios.GetES()->FindInstalledTMD(title.id);
716 const IOS::ES::TicketReader ticket = m_ios.GetES()->FindSignedTicket(title.id);
717
718 // Optional titles can be skipped if the ticket is present, even when the title isn't installed.
719 if (attrs.test(16) && ticket.IsValid())
720 return UpdateResult::AlreadyUpToDate;
721
722 // Otherwise, the title is only skipped if it is installed, its ticket is imported,
723 // and the installed version is new enough. No further checks unlike the online updater.
724 if (tmd.IsValid() && tmd.GetTitleVersion() >= title.version)
725 return UpdateResult::AlreadyUpToDate;
726
727 // Import the WAD.
728 auto blob = DiscIO::VolumeFileBlobReader::Create(*m_volume, m_partition, path);
729 if (!blob)
730 {
731 ERROR_LOG(CORE, "Could not find %s", std::string(path).c_str());
732 return UpdateResult::DiscReadFailed;
733 }
734 const DiscIO::VolumeWAD wad{std::move(blob)};
735 const bool success = ImportWAD(m_ios, wad, IOS::HLE::Device::ES::VerifySignature::Yes);
736 return success ? UpdateResult::Succeeded : UpdateResult::ImportFailed;
737 }
738
DoOnlineUpdate(UpdateCallback update_callback,const std::string & region)739 UpdateResult DoOnlineUpdate(UpdateCallback update_callback, const std::string& region)
740 {
741 OnlineSystemUpdater updater{std::move(update_callback), region};
742 return updater.DoOnlineUpdate();
743 }
744
DoDiscUpdate(UpdateCallback update_callback,const std::string & image_path)745 UpdateResult DoDiscUpdate(UpdateCallback update_callback, const std::string& image_path)
746 {
747 DiscSystemUpdater updater{std::move(update_callback), image_path};
748 return updater.DoDiscUpdate();
749 }
750
CheckNAND(IOS::HLE::Kernel & ios,bool repair)751 static NANDCheckResult CheckNAND(IOS::HLE::Kernel& ios, bool repair)
752 {
753 NANDCheckResult result;
754 const auto es = ios.GetES();
755
756 // Check for NANDs that were used with old Dolphin versions.
757 const std::string sys_replace_path =
758 Common::RootUserPath(Common::FROM_CONFIGURED_ROOT) + "/sys/replace";
759 if (File::Exists(sys_replace_path))
760 {
761 ERROR_LOG(CORE, "CheckNAND: NAND was used with old versions, so it is likely to be damaged");
762 if (repair)
763 File::Delete(sys_replace_path);
764 else
765 result.bad = true;
766 }
767
768 // Clean up after a bug fixed in https://github.com/dolphin-emu/dolphin/pull/8802
769 const std::string rfl_db_path = Common::GetMiiDatabasePath(Common::FROM_CONFIGURED_ROOT);
770 const File::FileInfo rfl_db(rfl_db_path);
771 if (rfl_db.Exists() && rfl_db.GetSize() == 0)
772 {
773 ERROR_LOG(CORE, "CheckNAND: RFL_DB.dat exists but is empty");
774 if (repair)
775 File::Delete(rfl_db_path);
776 else
777 result.bad = true;
778 }
779
780 for (const u64 title_id : es->GetInstalledTitles())
781 {
782 const std::string title_dir = Common::GetTitlePath(title_id, Common::FROM_CONFIGURED_ROOT);
783 const std::string content_dir = title_dir + "/content";
784 const std::string data_dir = title_dir + "/data";
785
786 // Check for missing title sub directories.
787 for (const std::string& dir : {content_dir, data_dir})
788 {
789 if (File::IsDirectory(dir))
790 continue;
791
792 ERROR_LOG(CORE, "CheckNAND: Missing dir %s for title %016" PRIx64, dir.c_str(), title_id);
793 if (repair)
794 File::CreateDir(dir);
795 else
796 result.bad = true;
797 }
798
799 // Check for incomplete title installs (missing ticket, TMD or contents).
800 const auto ticket = es->FindSignedTicket(title_id);
801 if (!IOS::ES::IsDiscTitle(title_id) && !ticket.IsValid())
802 {
803 ERROR_LOG(CORE, "CheckNAND: Missing ticket for title %016" PRIx64, title_id);
804 result.titles_to_remove.insert(title_id);
805 if (repair)
806 File::DeleteDirRecursively(title_dir);
807 else
808 result.bad = true;
809 }
810
811 const auto tmd = es->FindInstalledTMD(title_id);
812 if (!tmd.IsValid())
813 {
814 if (File::ScanDirectoryTree(content_dir, false).children.empty())
815 {
816 WARN_LOG(CORE, "CheckNAND: Missing TMD for title %016" PRIx64, title_id);
817 }
818 else
819 {
820 ERROR_LOG(CORE, "CheckNAND: Missing TMD for title %016" PRIx64, title_id);
821 result.titles_to_remove.insert(title_id);
822 if (repair)
823 File::DeleteDirRecursively(title_dir);
824 else
825 result.bad = true;
826 }
827 // Further checks require the TMD to be valid.
828 continue;
829 }
830
831 const auto installed_contents = es->GetStoredContentsFromTMD(tmd);
832 const bool is_installed = std::any_of(installed_contents.begin(), installed_contents.end(),
833 [](const auto& content) { return !content.IsShared(); });
834
835 if (is_installed && installed_contents != tmd.GetContents() &&
836 (tmd.GetTitleFlags() & IOS::ES::TitleFlags::TITLE_TYPE_DATA) == 0)
837 {
838 ERROR_LOG(CORE, "CheckNAND: Missing contents for title %016" PRIx64, title_id);
839 result.titles_to_remove.insert(title_id);
840 if (repair)
841 File::DeleteDirRecursively(title_dir);
842 else
843 result.bad = true;
844 }
845 }
846
847 return result;
848 }
849
CheckNAND(IOS::HLE::Kernel & ios)850 NANDCheckResult CheckNAND(IOS::HLE::Kernel& ios)
851 {
852 return CheckNAND(ios, false);
853 }
854
RepairNAND(IOS::HLE::Kernel & ios)855 bool RepairNAND(IOS::HLE::Kernel& ios)
856 {
857 return !CheckNAND(ios, true).bad;
858 }
859 } // namespace WiiUtils
860