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