12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475 |
- // Copyright 2019 Dolphin Emulator Project
- // SPDX-License-Identifier: GPL-2.0-or-later
- #include "DiscIO/VolumeVerifier.h"
- #include <algorithm>
- #include <future>
- #include <limits>
- #include <memory>
- #include <optional>
- #include <string>
- #include <string_view>
- #include <unordered_set>
- #include <mbedtls/md5.h>
- #include <mz_compat.h>
- #include <pugixml.hpp>
- #include "Common/Align.h"
- #include "Common/Assert.h"
- #include "Common/CPUDetect.h"
- #include "Common/CommonPaths.h"
- #include "Common/CommonTypes.h"
- #include "Common/Crypto/SHA1.h"
- #include "Common/FileUtil.h"
- #include "Common/Hash.h"
- #include "Common/HttpRequest.h"
- #include "Common/IOFile.h"
- #include "Common/Logging/Log.h"
- #include "Common/MinizipUtil.h"
- #include "Common/MsgHandler.h"
- #include "Common/ScopeGuard.h"
- #include "Common/StringUtil.h"
- #include "Common/Swap.h"
- #include "Common/Version.h"
- #include "Core/IOS/Device.h"
- #include "Core/IOS/ES/ES.h"
- #include "Core/IOS/ES/Formats.h"
- #include "Core/IOS/IOS.h"
- #include "Core/IOS/IOSC.h"
- #include "DiscIO/Blob.h"
- #include "DiscIO/DiscScrubber.h"
- #include "DiscIO/DiscUtils.h"
- #include "DiscIO/Enums.h"
- #include "DiscIO/Filesystem.h"
- #include "DiscIO/Volume.h"
- #include "DiscIO/VolumeWii.h"
- namespace DiscIO
- {
- RedumpVerifier::DownloadState RedumpVerifier::m_gc_download_state;
- RedumpVerifier::DownloadState RedumpVerifier::m_wii_download_state;
- void RedumpVerifier::Start(const Volume& volume)
- {
- if (!volume.IsDatelDisc())
- {
- m_game_id = volume.GetGameID();
- if (m_game_id.size() > 4)
- m_game_id = m_game_id.substr(0, 4);
- }
- m_revision = volume.GetRevision().value_or(0);
- m_disc_number = volume.GetDiscNumber().value_or(0);
- m_size = volume.GetDataSize();
- const DiscIO::Platform platform = volume.GetVolumeType();
- m_future = std::async(std::launch::async, [this, platform]() -> std::vector<PotentialMatch> {
- std::string system;
- DownloadState* download_state;
- switch (platform)
- {
- case Platform::GameCubeDisc:
- system = "gc";
- download_state = &m_gc_download_state;
- break;
- case Platform::WiiDisc:
- system = "wii";
- download_state = &m_wii_download_state;
- break;
- default:
- m_result.status = Status::Error;
- return {};
- }
- {
- std::lock_guard lk(download_state->mutex);
- download_state->status = DownloadDatfile(system, download_state->status);
- }
- switch (download_state->status)
- {
- case DownloadStatus::FailButOldCacheAvailable:
- ERROR_LOG_FMT(DISCIO, "Failed to fetch data from Redump.org, using old cached data instead");
- [[fallthrough]];
- case DownloadStatus::Success:
- return ScanDatfile(ReadDatfile(system), system);
- case DownloadStatus::SystemNotAvailable:
- m_result = {Status::Error, Common::GetStringT("Wii data is not public yet")};
- return {};
- case DownloadStatus::Fail:
- default:
- m_result = {Status::Error, Common::GetStringT("Failed to connect to Redump.org")};
- return {};
- }
- });
- }
- static std::string GetPathForSystem(const std::string& system)
- {
- return File::GetUserPath(D_REDUMPCACHE_IDX) + DIR_SEP + system + ".zip";
- }
- RedumpVerifier::DownloadStatus RedumpVerifier::DownloadDatfile(const std::string& system,
- DownloadStatus old_status)
- {
- if (old_status == DownloadStatus::Success || old_status == DownloadStatus::SystemNotAvailable)
- return old_status;
- Common::HttpRequest request;
- const std::optional<std::vector<u8>> result =
- request.Get("http://redump.org/datfile/" + system + "/serial,version",
- {{"User-Agent", Common::GetScmRevStr()}});
- const std::string output_path = GetPathForSystem(system);
- if (!result)
- {
- return File::Exists(output_path) ? DownloadStatus::FailButOldCacheAvailable :
- DownloadStatus::Fail;
- }
- if (result->size() > 1 && (*result)[0] == '<' && (*result)[1] == '!')
- {
- // This is an HTML page, not a zip file like we want
- if (File::Exists(output_path))
- return DownloadStatus::FailButOldCacheAvailable;
- const std::string system_not_available_message = "System \"" + system + "\" doesn't exist.";
- const bool system_not_available_match =
- result->end() != std::search(result->begin(), result->end(),
- system_not_available_message.begin(),
- system_not_available_message.end());
- return system_not_available_match ? DownloadStatus::SystemNotAvailable : DownloadStatus::Fail;
- }
- File::CreateFullPath(output_path);
- if (!File::IOFile(output_path, "wb").WriteBytes(result->data(), result->size()))
- ERROR_LOG_FMT(DISCIO, "Failed to write downloaded datfile to {}", output_path);
- return DownloadStatus::Success;
- }
- std::vector<u8> RedumpVerifier::ReadDatfile(const std::string& system)
- {
- unzFile file = unzOpen(GetPathForSystem(system).c_str());
- if (!file)
- return {};
- Common::ScopeGuard file_guard{[&] { unzClose(file); }};
- // Check that the zip file contains exactly one file
- if (unzGoToFirstFile(file) != UNZ_OK)
- return {};
- if (unzGoToNextFile(file) != UNZ_END_OF_LIST_OF_FILE)
- return {};
- // Read the file
- if (unzGoToFirstFile(file) != UNZ_OK)
- return {};
- unz_file_info file_info;
- unzGetCurrentFileInfo(file, &file_info, nullptr, 0, nullptr, 0, nullptr, 0);
- std::vector<u8> data(file_info.uncompressed_size);
- if (!Common::ReadFileFromZip(file, &data))
- return {};
- return data;
- }
- static u8 ParseHexDigit(char c)
- {
- if (c < '0')
- return 0; // Error
- if (c >= 'a')
- c -= 'a' - 'A';
- if (c >= 'A')
- c -= 'A' - ('9' + 1);
- c -= '0';
- if (c >= 0x10)
- return 0; // Error
- return c;
- }
- static std::vector<u8> ParseHash(const char* str)
- {
- std::vector<u8> hash;
- while (str[0] && str[1])
- {
- hash.push_back(static_cast<u8>(ParseHexDigit(str[0]) * 0x10 + ParseHexDigit(str[1])));
- str += 2;
- }
- return hash;
- }
- std::vector<RedumpVerifier::PotentialMatch> RedumpVerifier::ScanDatfile(const std::vector<u8>& data,
- const std::string& system)
- {
- pugi::xml_document doc;
- if (!doc.load_buffer(data.data(), data.size()))
- {
- m_result = {Status::Error, Common::GetStringT("Failed to parse Redump.org data")};
- return {};
- }
- std::vector<PotentialMatch> potential_matches;
- bool serials_exist = false;
- bool versions_exist = false;
- const pugi::xml_node datafile = doc.child("datafile");
- for (const pugi::xml_node game : datafile.children("game"))
- {
- std::string version_string = game.child("version").text().as_string();
- if (!version_string.empty())
- versions_exist = true;
- // Strip out prefix (e.g. "v1.02" -> "02", "Rev 2" -> "2")
- const size_t last_non_numeric = version_string.find_last_not_of("0123456789");
- if (last_non_numeric != std::string::npos)
- version_string = version_string.substr(last_non_numeric + 1);
- const int version = version_string.empty() ? 0 : std::stoi(version_string);
- const std::string serials = game.child("serial").text().as_string();
- if (!serials.empty())
- serials_exist = true;
- // The revisions for Korean GameCube games whose four-char game IDs end in E are numbered from
- // 0x30 in ring codes and in disc headers, but Redump switched to numbering them from 0 in 2019.
- if (version % 0x30 != m_revision % 0x30)
- continue;
- if (serials.empty() || serials.starts_with("DS"))
- {
- // GC Datel discs have no serials in Redump, Wii Datel discs have serials like "DS000101"
- if (!m_game_id.empty())
- continue; // Non-empty m_game_id means we're verifying a non-Datel disc
- }
- else
- {
- bool serial_match_found = false;
- // If a disc has multiple possible serials, they are delimited with ", ". We want to loop
- // through all the serials until we find a match, because even though they normally only
- // differ in the region code at the end (which we don't care about), there is an edge case
- // disc with the game ID "G96P" and the serial "DL-DOL-D96P-EUR, DL-DOL-G96P-EUR".
- for (const std::string& serial_str : SplitString(serials, ','))
- {
- const std::string_view serial = StripWhitespace(serial_str);
- // Skip the prefix, normally either "DL-DOL-" or "RVL-" (depending on the console),
- // but there are some exceptions like the "RVLE-SBSE-USA-B0" serial.
- const size_t first_dash = serial.find_first_of('-', 3);
- const size_t game_id_start =
- first_dash == std::string::npos ? std::string::npos : first_dash + 1;
- if (game_id_start == std::string::npos || serial.size() < game_id_start + 4)
- {
- ERROR_LOG_FMT(DISCIO, "Invalid serial in redump datfile: {}", serial_str);
- continue;
- }
- const std::string_view game_id = serial.substr(game_id_start, 4);
- if (game_id != m_game_id)
- continue;
- u8 disc_number = 0;
- if (serial.size() > game_id_start + 5 && serial[game_id_start + 5] >= '0' &&
- serial[game_id_start + 5] <= '9')
- {
- disc_number = serial[game_id_start + 5] - '0';
- }
- if (disc_number != m_disc_number)
- continue;
- serial_match_found = true;
- break;
- }
- if (!serial_match_found)
- continue;
- }
- PotentialMatch& potential_match = potential_matches.emplace_back();
- const pugi::xml_node rom = game.child("rom");
- potential_match.size = rom.attribute("size").as_ullong();
- potential_match.hashes.crc32 = ParseHash(rom.attribute("crc").value());
- potential_match.hashes.md5 = ParseHash(rom.attribute("md5").value());
- potential_match.hashes.sha1 = ParseHash(rom.attribute("sha1").value());
- }
- if (!serials_exist || !versions_exist)
- {
- // If we reach this, the user has most likely downloaded a datfile manually,
- // so show a panic alert rather than just using ERROR_LOG
- // i18n: "Serial" refers to serial numbers, e.g. RVL-RSBE-USA
- PanicAlertFmtT(
- "Serial and/or version data is missing from {0}\n"
- "Please append \"{1}\" (without the quotes) to the datfile URL when downloading\n"
- "Example: {2}",
- GetPathForSystem(system), "serial,version", "http://redump.org/datfile/gc/serial,version");
- m_result = {Status::Error, Common::GetStringT("Failed to parse Redump.org data")};
- return {};
- }
- return potential_matches;
- }
- static bool HashesMatch(const std::vector<u8>& calculated, const std::vector<u8>& expected)
- {
- return calculated.empty() || calculated == expected;
- }
- RedumpVerifier::Result RedumpVerifier::Finish(const Hashes<std::vector<u8>>& hashes)
- {
- if (m_result.status == Status::Error)
- return m_result;
- if (hashes.crc32.empty() && hashes.md5.empty() && hashes.sha1.empty())
- return m_result;
- const std::vector<PotentialMatch> potential_matches = m_future.get();
- for (PotentialMatch p : potential_matches)
- {
- if (HashesMatch(hashes.crc32, p.hashes.crc32) && HashesMatch(hashes.md5, p.hashes.md5) &&
- HashesMatch(hashes.sha1, p.hashes.sha1) && m_size == p.size)
- {
- return {Status::GoodDump, Common::GetStringT("Good dump")};
- }
- }
- // We only return bad dump if there's a disc that we know this dump should match but that doesn't
- // match. For disc without IDs (i.e. Datel discs), we don't have a good way of knowing whether we
- // have a bad dump or just a dump that isn't in Redump, so we always pick unknown instead of bad
- // dump for those to be on the safe side. (Besides, it's possible to dump a Datel disc correctly
- // and have it not match Redump if you don't use the same replacement value for bad sectors.)
- if (!potential_matches.empty() && !m_game_id.empty())
- return {Status::BadDump, Common::GetStringT("Bad dump")};
- return {Status::Unknown, Common::GetStringT("Unknown disc")};
- }
- constexpr u64 DEFAULT_READ_SIZE = 0x20000; // Arbitrary value
- VolumeVerifier::VolumeVerifier(const Volume& volume, bool redump_verification,
- Hashes<bool> hashes_to_calculate)
- : m_volume(volume), m_redump_verification(redump_verification),
- m_hashes_to_calculate(hashes_to_calculate),
- m_calculating_any_hash(hashes_to_calculate.crc32 || hashes_to_calculate.md5 ||
- hashes_to_calculate.sha1),
- m_max_progress(volume.GetDataSize()), m_data_size_type(volume.GetDataSizeType())
- {
- if (!m_calculating_any_hash)
- m_redump_verification = false;
- }
- VolumeVerifier::~VolumeVerifier()
- {
- WaitForAsyncOperations();
- }
- Hashes<bool> VolumeVerifier::GetDefaultHashesToCalculate()
- {
- Hashes<bool> hashes_to_calculate{.crc32 = true, .md5 = true, .sha1 = true};
- // If the system can compute certain hashes faster than others, only default-enable the fast ones.
- const bool sha1_hw_accel = Common::SHA1::CreateContext()->HwAccelerated();
- // For crc32, we assume zlib-ng will be fast if cpu supports crc32
- const bool crc32_hw_accel = cpu_info.bCRC32;
- if (crc32_hw_accel || sha1_hw_accel)
- {
- hashes_to_calculate.crc32 = crc32_hw_accel;
- // md5 has no accelerated implementation at the moment, always default to off
- hashes_to_calculate.md5 = false;
- // Always enable SHA1, to avoid situation where only crc32 is computed
- hashes_to_calculate.sha1 = true;
- }
- return hashes_to_calculate;
- }
- void VolumeVerifier::Start()
- {
- ASSERT(!m_started);
- m_started = true;
- if (m_redump_verification)
- m_redump_verifier.Start(m_volume);
- m_is_tgc = m_volume.GetBlobType() == BlobType::TGC;
- m_is_datel = m_volume.IsDatelDisc();
- m_is_not_retail = (m_volume.GetVolumeType() == Platform::WiiDisc && !m_volume.HasWiiHashes()) ||
- IsDebugSigned();
- const std::vector<Partition> partitions = CheckPartitions();
- if (IsDisc(m_volume.GetVolumeType()))
- m_biggest_referenced_offset = GetBiggestReferencedOffset(m_volume, partitions);
- CheckMisc();
- SetUpHashing();
- }
- std::vector<Partition> VolumeVerifier::CheckPartitions()
- {
- if (m_volume.GetVolumeType() == Platform::WiiWAD)
- return {};
- const std::vector<Partition> partitions = m_volume.GetPartitions();
- if (partitions.empty())
- {
- if (!m_volume.GetFileSystem(m_volume.GetGamePartition()))
- {
- AddProblem(Severity::High,
- Common::GetStringT("The filesystem is invalid or could not be read."));
- return {};
- }
- return {m_volume.GetGamePartition()};
- }
- std::optional<u32> partitions_in_first_table = m_volume.ReadSwapped<u32>(0x40000, PARTITION_NONE);
- if (partitions_in_first_table && *partitions_in_first_table > 8)
- {
- // Not sure if 8 actually is the limit, but there certainly aren't any discs
- // released that have as many partitions as 8 in the first partition table.
- // The only game that has that many partitions in total is Super Smash Bros. Brawl,
- // and that game places all partitions other than UPDATE and DATA in the second table.
- AddProblem(Severity::Low,
- Common::GetStringT("There are too many partitions in the first partition table."));
- }
- std::vector<u32> types;
- for (const Partition& partition : partitions)
- {
- const std::optional<u32> type = m_volume.GetPartitionType(partition);
- if (type)
- types.emplace_back(*type);
- }
- if (std::find(types.cbegin(), types.cend(), PARTITION_UPDATE) == types.cend())
- AddProblem(Severity::Low, Common::GetStringT("The update partition is missing."));
- const bool has_data_partition =
- std::find(types.cbegin(), types.cend(), PARTITION_DATA) != types.cend();
- if (!m_is_datel && !has_data_partition)
- AddProblem(Severity::High, Common::GetStringT("The data partition is missing."));
- const bool has_channel_partition =
- std::find(types.cbegin(), types.cend(), PARTITION_CHANNEL) != types.cend();
- if (ShouldHaveChannelPartition() && !has_channel_partition)
- AddProblem(Severity::Medium, Common::GetStringT("The channel partition is missing."));
- const bool has_install_partition =
- std::find(types.cbegin(), types.cend(), PARTITION_INSTALL) != types.cend();
- if (ShouldHaveInstallPartition() && !has_install_partition)
- AddProblem(Severity::High, Common::GetStringT("The install partition is missing."));
- if (ShouldHaveMasterpiecePartitions() &&
- types.cend() == std::ranges::find_if(types, [](u32 type) { return type >= 0xFF; }))
- {
- // i18n: This string is referring to a game mode in Super Smash Bros. Brawl called Masterpieces
- // where you play demos of NES/SNES/N64 games. Official translations:
- // 名作トライアル (Japanese), Masterpieces (English), Meisterstücke (German), Chefs-d'œuvre
- // (French), Clásicos (Spanish), Capolavori (Italian), 클래식 게임 체험판 (Korean).
- // If your language is not one of the languages above, consider leaving the string untranslated
- // so that people will recognize it as the name of the game mode.
- AddProblem(Severity::Medium, Common::GetStringT("The Masterpiece partitions are missing."));
- }
- for (const Partition& partition : partitions)
- {
- if (m_volume.GetPartitionType(partition) == PARTITION_UPDATE && partition.offset != 0x50000)
- {
- AddProblem(Severity::Low,
- Common::GetStringT("The update partition is not at its normal position."));
- }
- const u64 normal_data_offset = m_volume.HasWiiHashes() ? 0xF800000 : 0x838000;
- if (m_volume.GetPartitionType(partition) == PARTITION_DATA &&
- partition.offset != normal_data_offset && !has_channel_partition && !has_install_partition)
- {
- AddProblem(Severity::Low,
- Common::GetStringT(
- "The data partition is not at its normal position. This will affect the "
- "emulated loading times. You will be unable to share input recordings and use "
- "NetPlay with anyone who is using a good dump."));
- }
- }
- std::vector<Partition> valid_partitions;
- for (const Partition& partition : partitions)
- {
- if (CheckPartition(partition))
- valid_partitions.push_back(partition);
- }
- return valid_partitions;
- }
- bool VolumeVerifier::CheckPartition(const Partition& partition)
- {
- std::optional<u32> type = m_volume.GetPartitionType(partition);
- if (!type)
- {
- // Not sure if this can happen in practice
- AddProblem(Severity::Medium, Common::GetStringT("The type of a partition could not be read."));
- return false;
- }
- Severity severity = Severity::Medium;
- if (*type == PARTITION_DATA || *type == PARTITION_INSTALL)
- severity = Severity::High;
- else if (*type == PARTITION_UPDATE)
- severity = Severity::Low;
- const std::string name = GetPartitionName(type);
- if (partition.offset % VolumeWii::BLOCK_TOTAL_SIZE != 0 ||
- m_volume.PartitionOffsetToRawOffset(0, partition) % VolumeWii::BLOCK_TOTAL_SIZE != 0)
- {
- AddProblem(Severity::Medium,
- Common::FmtFormatT("The {0} partition is not properly aligned.", name));
- }
- bool invalid_header = false;
- bool blank_contents = false;
- std::vector<u8> disc_header(0x80);
- if (!m_volume.Read(0, disc_header.size(), disc_header.data(), partition))
- {
- invalid_header = true;
- }
- else if (Common::swap32(disc_header.data() + 0x18) != WII_DISC_MAGIC)
- {
- for (size_t i = 0; i < disc_header.size(); i += 4)
- {
- if (Common::swap32(disc_header.data() + i) != i)
- {
- invalid_header = true;
- break;
- }
- }
- // The loop above ends without setting invalid_header for discs that legitimately lack
- // updates. No such discs have been released to end users. Most such discs are debug signed,
- // but there is apparently at least one that is retail signed, the Movie-Ch Install Disc.
- if (!invalid_header)
- blank_contents = true;
- }
- if (invalid_header)
- {
- // This can happen when certain programs that create WBFS files scrub the entirety of
- // the Masterpiece partitions in Super Smash Bros. Brawl without removing them from
- // the partition table. https://bugs.dolphin-emu.org/issues/8733
- AddProblem(severity,
- Common::FmtFormatT("The {0} partition does not seem to contain valid data.", name));
- return false;
- }
- if (!m_is_datel)
- {
- const auto console_type =
- IsDebugSigned() ? IOS::HLE::IOSC::ConsoleType::RVT : IOS::HLE::IOSC::ConsoleType::Retail;
- IOS::HLE::Kernel ios(console_type);
- auto& es = ios.GetESCore();
- const std::vector<u8>& cert_chain = m_volume.GetCertificateChain(partition);
- if (IOS::HLE::IPC_SUCCESS !=
- es.VerifyContainer(IOS::HLE::ESCore::VerifyContainerType::Ticket,
- IOS::HLE::ESCore::VerifyMode::DoNotUpdateCertStore,
- m_volume.GetTicket(partition), cert_chain) ||
- IOS::HLE::IPC_SUCCESS !=
- es.VerifyContainer(IOS::HLE::ESCore::VerifyContainerType::TMD,
- IOS::HLE::ESCore::VerifyMode::DoNotUpdateCertStore,
- m_volume.GetTMD(partition), cert_chain))
- {
- AddProblem(Severity::Low,
- Common::FmtFormatT("The {0} partition is not correctly signed.", name));
- }
- }
- if (m_volume.HasWiiHashes() && !m_volume.CheckH3TableIntegrity(partition))
- {
- AddProblem(Severity::Low,
- Common::FmtFormatT("The H3 hash table for the {0} partition is not correct.", name));
- }
- // Prepare for hash verification in the Process step
- if (m_volume.HasWiiHashes())
- {
- const u64 data_size =
- m_volume.ReadSwappedAndShifted(partition.offset + 0x2bc, PARTITION_NONE).value_or(0);
- const size_t blocks = static_cast<size_t>(data_size / VolumeWii::BLOCK_TOTAL_SIZE);
- if (data_size % VolumeWii::BLOCK_TOTAL_SIZE != 0)
- {
- std::string text = Common::FmtFormatT(
- "The data size for the {0} partition is not evenly divisible by the block size.", name);
- AddProblem(Severity::Low, std::move(text));
- }
- u64 offset = m_volume.PartitionOffsetToRawOffset(0, partition);
- for (size_t block_index = 0; block_index < blocks;
- block_index += VolumeWii::BLOCKS_PER_GROUP, offset += VolumeWii::GROUP_TOTAL_SIZE)
- {
- m_groups.emplace_back(
- GroupToVerify{partition, offset, block_index,
- std::min(block_index + VolumeWii::BLOCKS_PER_GROUP, blocks)});
- }
- m_block_errors.emplace(partition, 0);
- }
- if (blank_contents)
- return false;
- const DiscIO::FileSystem* filesystem = m_volume.GetFileSystem(partition);
- if (!filesystem)
- {
- if (m_is_datel)
- {
- // Datel's Wii Freeloader has an invalid FST in its only partition
- return true;
- }
- AddProblem(severity,
- Common::FmtFormatT("The {0} partition does not have a valid file system.", name));
- return false;
- }
- if (type == PARTITION_UPDATE)
- {
- const IOS::ES::TMDReader& tmd = m_volume.GetTMD(m_volume.GetGamePartition());
- // IOS9 is the only IOS which can be assumed to exist in a working state on any Wii
- // regardless of what updates have been installed. At least Mario Party 8
- // (RM8E01, revision 2) uses IOS9 without having it in its update partition.
- const u64 ios_ver = tmd.GetIOSId() & 0xFF;
- bool has_correct_ios = tmd.IsValid() && ios_ver == 9;
- if (!has_correct_ios && tmd.IsValid())
- {
- std::unique_ptr<FileInfo> file_info = filesystem->FindFileInfo("_sys");
- if (file_info)
- {
- const std::string ios_ver_str = std::to_string(ios_ver);
- const std::string correct_ios =
- IsDebugSigned() ? ("firmware.64." + ios_ver_str + ".") : ("ios" + ios_ver_str + "-");
- for (const FileInfo& f : *file_info)
- {
- std::string file_name = f.GetName();
- Common::ToLower(&file_name);
- if (file_name.starts_with(correct_ios))
- {
- has_correct_ios = true;
- break;
- }
- }
- }
- }
- if (!has_correct_ios)
- {
- // This is reached for hacked dumps where the update partition has been replaced with
- // a very old update partition so that no updates will be installed.
- AddProblem(
- Severity::Low,
- Common::GetStringT("The update partition does not contain the IOS used by this title."));
- }
- }
- return true;
- }
- std::string VolumeVerifier::GetPartitionName(std::optional<u32> type) const
- {
- if (!type)
- return "???";
- std::string name = NameForPartitionType(*type, false);
- if (ShouldHaveMasterpiecePartitions() && *type > 0xFF)
- {
- // i18n: This string is referring to a game mode in Super Smash Bros. Brawl called Masterpieces
- // where you play demos of NES/SNES/N64 games. This string is referring to a specific such demo
- // rather than the game mode as a whole, so please use the singular form. Official translations:
- // 名作トライアル (Japanese), Masterpieces (English), Meisterstücke (German), Chefs-d'œuvre
- // (French), Clásicos (Spanish), Capolavori (Italian), 클래식 게임 체험판 (Korean).
- // If your language is not one of the languages above, consider leaving the string untranslated
- // so that people will recognize it as the name of the game mode.
- name = Common::FmtFormatT("{0} (Masterpiece)", name);
- }
- return name;
- }
- bool VolumeVerifier::IsDebugSigned() const
- {
- const IOS::ES::TicketReader& ticket = m_volume.GetTicket(m_volume.GetGamePartition());
- return ticket.IsValid() ? ticket.GetConsoleType() == IOS::HLE::IOSC::ConsoleType::RVT : false;
- }
- bool VolumeVerifier::ShouldHaveChannelPartition() const
- {
- static constexpr std::array<std::string_view, 18> channel_discs = {
- "RFNE01", "RFNJ01", "RFNK01", "RFNP01", "RFNW01", "RFPE01", "RFPJ01", "RFPK01", "RFPP01",
- "RFPW01", "RGWE41", "RGWJ41", "RGWP41", "RGWX41", "RMCE01", "RMCJ01", "RMCK01", "RMCP01",
- };
- static_assert(std::ranges::is_sorted(channel_discs));
- return std::ranges::binary_search(channel_discs, m_volume.GetGameID());
- }
- bool VolumeVerifier::ShouldHaveInstallPartition() const
- {
- static constexpr std::array<std::string_view, 4> dragon_quest_x = {"S4MJGD", "S4SJGD", "S6TJGD",
- "SDQJGD"};
- const std::string& game_id = m_volume.GetGameID();
- return std::any_of(dragon_quest_x.cbegin(), dragon_quest_x.cend(),
- [&game_id](std::string_view x) { return x == game_id; });
- }
- bool VolumeVerifier::ShouldHaveMasterpiecePartitions() const
- {
- static constexpr std::array<std::string_view, 4> ssbb = {"RSBE01", "RSBJ01", "RSBK01", "RSBP01"};
- const std::string& game_id = m_volume.GetGameID();
- return std::any_of(ssbb.cbegin(), ssbb.cend(),
- [&game_id](std::string_view x) { return x == game_id; });
- }
- bool VolumeVerifier::ShouldBeDualLayer() const
- {
- // The Japanese versions of Xenoblade and The Last Story are single-layer
- // (unlike the other versions) and must not be added to this list.
- static constexpr std::array<std::string_view, 33> dual_layer_discs = {
- "R3ME01", "R3MP01", "R3OE01", "R3OJ01", "R3OP01", "RSBE01", "RSBJ01", "RSBK01", "RSBP01",
- "RXMJ8P", "S59E01", "S59JC8", "S59P01", "S5QJC8", "SAKENS", "SAKPNS", "SK8V52", "SK8X52",
- "SLSEXJ", "SLSP01", "SQIE4Q", "SQIP4Q", "SQIY4Q", "SR5E41", "SR5P41", "SUOE41", "SUOP41",
- "SVXX52", "SVXY52", "SX4E01", "SX4P01", "SZ3EGT", "SZ3PGT",
- };
- static_assert(std::ranges::is_sorted(dual_layer_discs));
- return std::ranges::binary_search(dual_layer_discs, m_volume.GetGameID());
- }
- void VolumeVerifier::CheckVolumeSize()
- {
- u64 volume_size = m_volume.GetDataSize();
- const bool is_disc = IsDisc(m_volume.GetVolumeType());
- const bool should_be_dual_layer = is_disc && ShouldBeDualLayer();
- bool volume_size_roughly_known = m_data_size_type != DiscIO::DataSizeType::UpperBound;
- if (should_be_dual_layer && m_biggest_referenced_offset <= SL_DVD_R_SIZE)
- {
- AddProblem(Severity::Medium,
- Common::GetStringT(
- "This game has been hacked to fit on a single-layer DVD. Some content such as "
- "pre-rendered videos, extra languages or entire game modes will be broken. "
- "This problem generally only exists in illegal copies of games."));
- }
- if (m_data_size_type != DiscIO::DataSizeType::Accurate)
- {
- AddProblem(Severity::Low,
- Common::GetStringT("The format that the disc image is saved in does not "
- "store the size of the disc image."));
- if (!volume_size_roughly_known && m_volume.HasWiiHashes())
- {
- volume_size = m_biggest_verified_offset;
- volume_size_roughly_known = true;
- }
- }
- if (m_content_index != m_content_offsets.size() || m_group_index != m_groups.size() ||
- (!m_is_datel && volume_size_roughly_known && m_biggest_referenced_offset > volume_size))
- {
- const bool second_layer_missing = is_disc && volume_size_roughly_known &&
- volume_size >= SL_DVD_SIZE && volume_size <= SL_DVD_R_SIZE;
- std::string text =
- second_layer_missing ?
- Common::GetStringT("This disc image is too small and lacks some data. The problem is "
- "most likely that this is a dual-layer disc that has been dumped "
- "as a single-layer disc.") :
- Common::GetStringT("This disc image is too small and lacks some data. If your "
- "dumping program saved the disc image as several parts, you need "
- "to merge them into one file.");
- AddProblem(Severity::High, std::move(text));
- return;
- }
- // The reason why this condition is checking for m_data_size_type != UpperBound instead of
- // m_data_size_type == Accurate is because we want to show the warning about input recordings and
- // NetPlay for NFS disc images (which are the only disc images that have it set to LowerBound).
- if (is_disc && m_data_size_type != DiscIO::DataSizeType::UpperBound && !m_is_tgc)
- {
- const Platform platform = m_volume.GetVolumeType();
- const bool should_be_gc_size = platform == Platform::GameCubeDisc || m_is_datel;
- const bool valid_gamecube = volume_size == MINI_DVD_SIZE;
- const bool valid_retail_wii = volume_size == SL_DVD_SIZE || volume_size == DL_DVD_SIZE;
- const bool valid_debug_wii = volume_size == SL_DVD_R_SIZE || volume_size == DL_DVD_R_SIZE;
- const bool debug = IsDebugSigned();
- if ((should_be_gc_size && !valid_gamecube) ||
- (!should_be_gc_size && (debug ? !valid_debug_wii : !valid_retail_wii)))
- {
- if (debug && valid_retail_wii)
- {
- AddProblem(
- Severity::Low,
- Common::GetStringT("This debug disc image has the size of a retail disc image."));
- }
- else
- {
- u64 normal_size;
- if (should_be_gc_size)
- normal_size = MINI_DVD_SIZE;
- else if (!should_be_dual_layer)
- normal_size = SL_DVD_SIZE;
- else
- normal_size = DL_DVD_SIZE;
- if (volume_size < normal_size)
- {
- AddProblem(
- Severity::Low,
- Common::GetStringT(
- "This disc image has an unusual size. This will likely make the emulated "
- "loading times longer. You will likely be unable to share input recordings "
- "and use NetPlay with anyone who is using a good dump."));
- }
- else
- {
- AddProblem(Severity::Low, Common::GetStringT("This disc image has an unusual size."));
- }
- }
- }
- }
- }
- void VolumeVerifier::CheckMisc()
- {
- const std::string game_id_unencrypted = m_volume.GetGameID(PARTITION_NONE);
- const std::string game_id_encrypted = m_volume.GetGameID(m_volume.GetGamePartition());
- if (game_id_unencrypted != game_id_encrypted)
- {
- bool inconsistent_game_id = true;
- if (game_id_encrypted == "RELSAB")
- {
- if (game_id_unencrypted.starts_with("410"))
- {
- // This is the Wii Backup Disc (aka "pinkfish" disc),
- // which legitimately has an inconsistent game ID.
- inconsistent_game_id = false;
- }
- else if (game_id_unencrypted.starts_with("010"))
- {
- // Hacked version of the Wii Backup Disc (aka "pinkfish" disc).
- std::string proper_game_id = game_id_unencrypted;
- proper_game_id[0] = '4';
- AddProblem(Severity::Low, Common::FmtFormatT("The game ID is {0} but should be {1}.",
- game_id_unencrypted, proper_game_id));
- inconsistent_game_id = false;
- }
- }
- if (inconsistent_game_id)
- {
- AddProblem(Severity::Low, Common::GetStringT("The game ID is inconsistent."));
- }
- }
- const Region region = m_volume.GetRegion();
- constexpr std::string_view GAMECUBE_PLACEHOLDER_ID = "RELSAB";
- constexpr std::string_view WII_PLACEHOLDER_ID = "RABAZZ";
- if (game_id_encrypted.size() < 4)
- {
- AddProblem(Severity::Low, Common::GetStringT("The game ID is unusually short."));
- }
- else if (!m_is_datel && game_id_encrypted != GAMECUBE_PLACEHOLDER_ID &&
- game_id_encrypted != WII_PLACEHOLDER_ID)
- {
- char country_code;
- if (IsDisc(m_volume.GetVolumeType()))
- country_code = game_id_encrypted[3];
- else
- country_code = static_cast<char>(m_volume.GetTitleID().value_or(0) & 0xff);
- const Platform platform = m_volume.GetVolumeType();
- const std::optional<u16> revision = m_volume.GetRevision();
- if (CountryCodeToRegion(country_code, platform, region, revision) != region)
- {
- AddProblem(Severity::Medium,
- Common::GetStringT(
- "The region code does not match the game ID. If this is because the "
- "region code has been modified, the game might run at the wrong speed, "
- "graphical elements might be offset, or the game might not run at all."));
- }
- }
- const IOS::ES::TMDReader& tmd = m_volume.GetTMD(m_volume.GetGamePartition());
- if (tmd.IsValid())
- {
- const u64 ios_id = tmd.GetIOSId() & 0xFF;
- // List of launch day Korean IOSes obtained from https://hackmii.com/2008/09/korean-wii/.
- // More IOSes were released later that were used in Korean games, but they're all over 40.
- // Also, old IOSes like IOS36 did eventually get released for Korean Wiis as part of system
- // updates, but there are likely no Korean games using them since those IOSes were old by then.
- if (region == Region::NTSC_K && ios_id < 40 && ios_id != 4 && ios_id != 9 && ios_id != 21 &&
- ios_id != 37)
- {
- // This is intended to catch Korean games (usually but not always pirated) that have the IOS
- // slot set to 36 as a side effect of having to fakesign after changing the common key slot to
- // 0. (IOS36 was the last IOS with the Trucha bug.) https://bugs.dolphin-emu.org/issues/10319
- AddProblem(
- Severity::High,
- // i18n: You may want to leave the term "ERROR #002" untranslated,
- // since the emulated software always displays it in English.
- Common::GetStringT("This Korean title is set to use an IOS that typically isn't used on "
- "Korean consoles. This is likely to lead to ERROR #002."));
- }
- if (ios_id >= 0x80)
- {
- // This is intended to catch the same kind of fakesigned Korean games,
- // but this time with the IOS slot set to cIOS instead of IOS36.
- AddProblem(Severity::High, Common::GetStringT("This title is set to use an invalid IOS."));
- }
- }
- m_ticket = m_volume.GetTicket(m_volume.GetGamePartition());
- if (m_ticket.IsValid())
- {
- const u8 specified_common_key_index = m_ticket.GetCommonKeyIndex();
- // Wii discs only use common key 0 (regular) and common key 1 (Korean), not common key 2 (vWii).
- if (m_volume.GetVolumeType() == Platform::WiiDisc && specified_common_key_index > 1)
- {
- AddProblem(Severity::High,
- // i18n: This is "common" as in "shared", not the opposite of "uncommon"
- Common::GetStringT("This title is set to use an invalid common key."));
- }
- if (m_volume.GetVolumeType() == Platform::WiiWAD)
- {
- m_ticket = m_volume.GetTicketWithFixedCommonKey();
- const u8 fixed_common_key_index = m_ticket.GetCommonKeyIndex();
- if (specified_common_key_index != fixed_common_key_index)
- {
- // Many fakesigned WADs have the common key index set to a (random?) bogus value.
- // For WADs, Dolphin will detect this and use the correct key, making this low severity.
- AddProblem(Severity::Low,
- // i18n: This is "common" as in "shared", not the opposite of "uncommon"
- Common::FmtFormatT("The specified common key index is {0} but should be {1}.",
- specified_common_key_index, fixed_common_key_index));
- }
- }
- }
- if (m_volume.GetVolumeType() == Platform::WiiWAD)
- {
- IOS::HLE::Kernel ios(m_ticket.GetConsoleType());
- auto& es = ios.GetESCore();
- const std::vector<u8>& cert_chain = m_volume.GetCertificateChain(PARTITION_NONE);
- if (IOS::HLE::IPC_SUCCESS !=
- es.VerifyContainer(IOS::HLE::ESCore::VerifyContainerType::Ticket,
- IOS::HLE::ESCore::VerifyMode::DoNotUpdateCertStore, m_ticket,
- cert_chain))
- {
- // i18n: "Ticket" here is a kind of digital authorization to use a certain title (e.g. a game)
- AddProblem(Severity::Low, Common::GetStringT("The ticket is not correctly signed."));
- }
- if (IOS::HLE::IPC_SUCCESS !=
- es.VerifyContainer(IOS::HLE::ESCore::VerifyContainerType::TMD,
- IOS::HLE::ESCore::VerifyMode::DoNotUpdateCertStore, tmd, cert_chain))
- {
- AddProblem(
- Severity::Medium,
- Common::GetStringT("The TMD is not correctly signed. If you move or copy this title to "
- "the SD Card, the Wii System Menu will not launch it anymore and will "
- "also refuse to copy or move it back to the NAND."));
- }
- }
- if (m_volume.IsNKit())
- {
- AddProblem(
- Severity::Low,
- Common::GetStringT("This disc image is in the NKit format. It is not a good dump in its "
- "current form, but it might become a good dump if converted back. "
- "The CRC32 of this file might match the CRC32 of a good dump even "
- "though the files are not identical."));
- }
- if (IsDisc(m_volume.GetVolumeType()) && game_id_unencrypted.starts_with("R8P"))
- CheckSuperPaperMario();
- }
- void VolumeVerifier::CheckSuperPaperMario()
- {
- // When Super Paper Mario (any region/revision) reads setup/aa1_01.dat when starting a new game,
- // it also reads a few extra bytes so that the read length is divisible by 0x20. If these extra
- // bytes are zeroes like in good dumps, the game works correctly, but otherwise it can freeze
- // (depending on the exact values of the extra bytes). https://bugs.dolphin-emu.org/issues/11900
- const DiscIO::Partition partition = m_volume.GetGamePartition();
- const FileSystem* fs = m_volume.GetFileSystem(partition);
- if (!fs)
- return;
- std::unique_ptr<FileInfo> file_info = fs->FindFileInfo("setup/aa1_01.dat");
- if (!file_info)
- return;
- const u64 offset = file_info->GetOffset() + file_info->GetSize();
- const u64 length = Common::AlignUp(offset, 0x20) - offset;
- std::vector<u8> data(length);
- if (!m_volume.Read(offset, length, data.data(), partition))
- return;
- if (std::any_of(data.cbegin(), data.cend(), [](u8 x) { return x != 0; }))
- {
- AddProblem(Severity::High,
- Common::GetStringT("Some padding data that should be zero is not zero. "
- "This can make the game freeze at certain points."));
- }
- }
- void VolumeVerifier::SetUpHashing()
- {
- if (m_volume.GetVolumeType() == Platform::WiiWAD)
- {
- m_content_offsets = m_volume.GetContentOffsets();
- }
- else if (m_volume.GetVolumeType() == Platform::WiiDisc)
- {
- // Set up a DiscScrubber for checking whether blocks with errors are unused
- m_scrubber.SetupScrub(m_volume);
- }
- std::sort(m_groups.begin(), m_groups.end(),
- [](const GroupToVerify& a, const GroupToVerify& b) { return a.offset < b.offset; });
- if (m_hashes_to_calculate.crc32)
- m_crc32_context = Common::StartCRC32();
- if (m_hashes_to_calculate.md5)
- {
- mbedtls_md5_init(&m_md5_context);
- mbedtls_md5_starts_ret(&m_md5_context);
- }
- if (m_hashes_to_calculate.sha1)
- {
- m_sha1_context = Common::SHA1::CreateContext();
- }
- }
- void VolumeVerifier::WaitForAsyncOperations() const
- {
- if (m_crc32_future.valid())
- m_crc32_future.wait();
- if (m_md5_future.valid())
- m_md5_future.wait();
- if (m_sha1_future.valid())
- m_sha1_future.wait();
- if (m_content_future.valid())
- m_content_future.wait();
- if (m_group_future.valid())
- m_group_future.wait();
- }
- bool VolumeVerifier::ReadChunkAndWaitForAsyncOperations(u64 bytes_to_read)
- {
- std::vector<u8> data(bytes_to_read);
- const u64 bytes_to_copy = std::min(m_excess_bytes, bytes_to_read);
- if (bytes_to_copy > 0)
- std::memcpy(data.data(), m_data.data() + m_data.size() - m_excess_bytes, bytes_to_copy);
- bytes_to_read -= bytes_to_copy;
- if (bytes_to_read > 0)
- {
- if (!m_volume.Read(m_progress + bytes_to_copy, bytes_to_read, data.data() + bytes_to_copy,
- PARTITION_NONE))
- {
- return false;
- }
- }
- WaitForAsyncOperations();
- m_data = std::move(data);
- return true;
- }
- void VolumeVerifier::Process()
- {
- ASSERT(m_started);
- ASSERT(!m_done);
- if (m_progress >= m_max_progress)
- return;
- IOS::ES::Content content{};
- bool content_read = false;
- bool group_read = false;
- u64 bytes_to_read = DEFAULT_READ_SIZE;
- u64 excess_bytes = 0;
- if (m_content_index < m_content_offsets.size() &&
- m_content_offsets[m_content_index] == m_progress)
- {
- m_volume.GetTMD(PARTITION_NONE).GetContent(m_content_index, &content);
- bytes_to_read = Common::AlignUp(content.size, 0x40);
- content_read = true;
- const u16 next_content_index = m_content_index + 1;
- if (next_content_index < m_content_offsets.size() &&
- m_content_offsets[next_content_index] < m_progress + bytes_to_read)
- {
- excess_bytes = m_progress + bytes_to_read - m_content_offsets[next_content_index];
- }
- }
- else if (m_content_index < m_content_offsets.size() &&
- m_content_offsets[m_content_index] > m_progress)
- {
- bytes_to_read = std::min(bytes_to_read, m_content_offsets[m_content_index] - m_progress);
- }
- else if (m_group_index < m_groups.size() && m_groups[m_group_index].offset == m_progress)
- {
- const size_t blocks =
- m_groups[m_group_index].block_index_end - m_groups[m_group_index].block_index_start;
- bytes_to_read = VolumeWii::BLOCK_TOTAL_SIZE * blocks;
- group_read = true;
- if (m_group_index + 1 < m_groups.size() &&
- m_groups[m_group_index + 1].offset < m_progress + bytes_to_read)
- {
- excess_bytes = m_progress + bytes_to_read - m_groups[m_group_index + 1].offset;
- }
- }
- else if (m_group_index < m_groups.size() && m_groups[m_group_index].offset > m_progress)
- {
- bytes_to_read = std::min(bytes_to_read, m_groups[m_group_index].offset - m_progress);
- }
- if (m_progress + bytes_to_read > m_max_progress)
- {
- const u64 bytes_over_max = m_progress + bytes_to_read - m_max_progress;
- if (m_data_size_type == DataSizeType::LowerBound)
- {
- // Disc images in NFS format can have the last referenced block be past m_max_progress.
- // For NFS, reading beyond m_max_progress doesn't return an error, so let's read beyond it.
- excess_bytes = std::max(excess_bytes, bytes_over_max);
- }
- else
- {
- // Don't read beyond the end of the disc.
- bytes_to_read -= bytes_over_max;
- excess_bytes -= std::min(excess_bytes, bytes_over_max);
- content_read = false;
- group_read = false;
- }
- }
- const bool is_data_needed = m_calculating_any_hash || content_read || group_read;
- const bool read_failed = is_data_needed && !ReadChunkAndWaitForAsyncOperations(bytes_to_read);
- if (read_failed)
- {
- ERROR_LOG_FMT(DISCIO, "Read failed at {:#x} to {:#x}", m_progress, m_progress + bytes_to_read);
- m_read_errors_occurred = true;
- m_calculating_any_hash = false;
- }
- m_excess_bytes = excess_bytes;
- const u64 byte_increment = bytes_to_read - excess_bytes;
- if (m_calculating_any_hash)
- {
- if (m_hashes_to_calculate.crc32)
- {
- m_crc32_future = std::async(std::launch::async, [this, byte_increment] {
- m_crc32_context = Common::UpdateCRC32(m_crc32_context, m_data.data(),
- static_cast<size_t>(byte_increment));
- });
- }
- if (m_hashes_to_calculate.md5)
- {
- m_md5_future = std::async(std::launch::async, [this, byte_increment] {
- mbedtls_md5_update_ret(&m_md5_context, m_data.data(), byte_increment);
- });
- }
- if (m_hashes_to_calculate.sha1)
- {
- m_sha1_future = std::async(std::launch::async, [this, byte_increment] {
- m_sha1_context->Update(m_data.data(), byte_increment);
- });
- }
- }
- if (content_read)
- {
- m_content_future = std::async(std::launch::async, [this, read_failed, content] {
- if (read_failed || !m_volume.CheckContentIntegrity(content, m_data, m_ticket))
- {
- AddProblem(Severity::High, Common::FmtFormatT("Content {0:08x} is corrupt.", content.id));
- }
- });
- m_content_index++;
- }
- if (group_read)
- {
- m_group_future = std::async(std::launch::async, [this, read_failed,
- group_index = m_group_index] {
- const GroupToVerify& group = m_groups[group_index];
- u64 offset_in_group = 0;
- for (u64 block_index = group.block_index_start; block_index < group.block_index_end;
- ++block_index, offset_in_group += VolumeWii::BLOCK_TOTAL_SIZE)
- {
- const u64 block_offset = group.offset + offset_in_group;
- if (!read_failed && m_volume.CheckBlockIntegrity(
- block_index, m_data.data() + offset_in_group, group.partition))
- {
- m_biggest_verified_offset =
- std::max(m_biggest_verified_offset, block_offset + VolumeWii::BLOCK_TOTAL_SIZE);
- }
- else
- {
- if (m_scrubber.CanBlockBeScrubbed(block_offset))
- {
- WARN_LOG_FMT(DISCIO, "Integrity check failed for unused block at {:#x}", block_offset);
- m_unused_block_errors[group.partition]++;
- }
- else
- {
- WARN_LOG_FMT(DISCIO, "Integrity check failed for block at {:#x}", block_offset);
- m_block_errors[group.partition]++;
- }
- }
- }
- });
- m_group_index++;
- }
- m_progress += byte_increment;
- }
- u64 VolumeVerifier::GetBytesProcessed() const
- {
- return m_progress;
- }
- u64 VolumeVerifier::GetTotalBytes() const
- {
- return m_max_progress;
- }
- void VolumeVerifier::Finish()
- {
- if (m_done)
- return;
- m_done = true;
- WaitForAsyncOperations();
- if (m_calculating_any_hash)
- {
- if (m_hashes_to_calculate.crc32)
- {
- m_result.hashes.crc32 = std::vector<u8>(4);
- const u32 crc32_be = Common::swap32(m_crc32_context);
- std::memcpy(m_result.hashes.crc32.data(), &crc32_be, 4);
- }
- if (m_hashes_to_calculate.md5)
- {
- m_result.hashes.md5 = std::vector<u8>(16);
- mbedtls_md5_finish_ret(&m_md5_context, m_result.hashes.md5.data());
- }
- if (m_hashes_to_calculate.sha1)
- {
- const auto digest = m_sha1_context->Finish();
- m_result.hashes.sha1 = std::vector<u8>(digest.begin(), digest.end());
- }
- }
- if (m_read_errors_occurred)
- AddProblem(Severity::Medium, Common::GetStringT("Some of the data could not be read."));
- CheckVolumeSize();
- for (auto [partition, blocks] : m_block_errors)
- {
- if (blocks > 0)
- {
- const std::string name = GetPartitionName(m_volume.GetPartitionType(partition));
- AddProblem(Severity::Medium,
- Common::FmtFormatT("Errors were found in {0} blocks in the {1} partition.", blocks,
- name));
- }
- }
- for (auto [partition, blocks] : m_unused_block_errors)
- {
- if (blocks > 0)
- {
- const std::string name = GetPartitionName(m_volume.GetPartitionType(partition));
- AddProblem(Severity::Low,
- Common::FmtFormatT("Errors were found in {0} unused blocks in the {1} partition.",
- blocks, name));
- }
- }
- // Show the most serious problems at the top
- std::stable_sort(m_result.problems.begin(), m_result.problems.end(),
- [](const Problem& p1, const Problem& p2) { return p1.severity > p2.severity; });
- const Severity highest_severity =
- m_result.problems.empty() ? Severity::None : m_result.problems[0].severity;
- if (m_redump_verification)
- m_result.redump = m_redump_verifier.Finish(m_result.hashes);
- if (m_result.redump.status == RedumpVerifier::Status::GoodDump ||
- (m_volume.GetVolumeType() == Platform::WiiWAD && !m_is_not_retail &&
- m_result.problems.empty()))
- {
- if (m_result.problems.empty())
- {
- m_result.summary_text = Common::GetStringT("This is a good dump.");
- }
- else
- {
- m_result.summary_text =
- Common::GetStringT("This is a good dump according to Redump.org, but Dolphin has found "
- "problems. This might be a bug in Dolphin.");
- }
- return;
- }
- if (m_is_datel)
- {
- m_result.summary_text = Common::GetStringT("Dolphin is unable to verify unlicensed discs.");
- return;
- }
- if (m_is_tgc)
- {
- m_result.summary_text =
- Common::GetStringT("Dolphin is unable to verify typical TGC files properly, "
- "since they are not dumps of actual discs.");
- return;
- }
- if (m_result.redump.status == RedumpVerifier::Status::BadDump &&
- highest_severity <= Severity::Low)
- {
- if (m_volume.GetBlobType() == BlobType::NFS)
- {
- m_result.summary_text =
- Common::GetStringT("Compared to the Wii disc release of the game, this is a bad dump. "
- "Despite this, it's possible that this is a good dump compared to the "
- "Wii U eShop release of the game. Dolphin can't verify this.");
- }
- else
- {
- m_result.summary_text = Common::GetStringT(
- "This is a bad dump. This doesn't necessarily mean that the game won't run correctly.");
- }
- }
- else
- {
- if (m_result.redump.status == RedumpVerifier::Status::BadDump)
- {
- m_result.summary_text = Common::GetStringT("This is a bad dump.") + "\n\n";
- }
- switch (highest_severity)
- {
- case Severity::None:
- if (IsWii(m_volume.GetVolumeType()) && !m_is_not_retail)
- {
- m_result.summary_text = Common::GetStringT(
- "No problems were found. This does not guarantee that this is a good dump, "
- "but since Wii titles contain a lot of verification data, it does mean that "
- "there most likely are no problems that will affect emulation.");
- }
- else
- {
- m_result.summary_text = Common::GetStringT("No problems were found.");
- }
- break;
- case Severity::Low:
- if (m_volume.GetBlobType() == BlobType::NFS)
- {
- m_result.summary_text = Common::GetStringT(
- "Compared to the Wii disc release of the game, problems of low severity were found. "
- "Despite this, it's possible that this is a good dump compared to the Wii U eShop "
- "release of the game. Dolphin can't verify this.");
- }
- else
- {
- m_result.summary_text =
- Common::GetStringT("Problems with low severity were found. They will most "
- "likely not prevent the game from running.");
- }
- break;
- case Severity::Medium:
- m_result.summary_text +=
- Common::GetStringT("Problems with medium severity were found. The whole game "
- "or certain parts of the game might not work correctly.");
- break;
- case Severity::High:
- m_result.summary_text += Common::GetStringT(
- "Problems with high severity were found. The game will most likely not work at all.");
- break;
- }
- }
- if (m_volume.GetVolumeType() == Platform::GameCubeDisc)
- {
- m_result.summary_text +=
- Common::GetStringT("\n\nBecause GameCube disc images contain little verification data, "
- "there may be problems that Dolphin is unable to detect.");
- }
- else if (m_is_not_retail)
- {
- m_result.summary_text +=
- Common::GetStringT("\n\nBecause this title is not for retail Wii consoles, "
- "Dolphin cannot ensure that it hasn't been tampered with, even if "
- "signatures appear valid.");
- }
- }
- const VolumeVerifier::Result& VolumeVerifier::GetResult() const
- {
- return m_result;
- }
- void VolumeVerifier::AddProblem(Severity severity, std::string text)
- {
- m_result.problems.emplace_back(Problem{severity, std::move(text)});
- }
- } // namespace DiscIO
|