summaryrefslogtreecommitdiff
path: root/Source/Core/DiscIO/VolumeVerifier.cpp
diff options
context:
space:
mode:
authorJosJuice <josjuice@gmail.com>2019-03-21 23:04:56 +0100
committerJosJuice <josjuice@gmail.com>2019-04-09 14:18:41 +0200
commit84cbd5150f62bd5dd4a8d621fd0ccf7623ca4e2c (patch)
tree4a666bce2e24563a0282b66f4d4f907107f03fd0 /Source/Core/DiscIO/VolumeVerifier.cpp
parentc885fed9da6f6b0c5862b99904bbb5edbba144fe (diff)
Add a Verify tab to game properties
Diffstat (limited to 'Source/Core/DiscIO/VolumeVerifier.cpp')
-rw-r--r--Source/Core/DiscIO/VolumeVerifier.cpp689
1 files changed, 689 insertions, 0 deletions
diff --git a/Source/Core/DiscIO/VolumeVerifier.cpp b/Source/Core/DiscIO/VolumeVerifier.cpp
new file mode 100644
index 0000000000..7d3eba4c8f
--- /dev/null
+++ b/Source/Core/DiscIO/VolumeVerifier.cpp
@@ -0,0 +1,689 @@
+// Copyright 2019 Dolphin Emulator Project
+// Licensed under GPLv2+
+// Refer to the license.txt file included.
+
+#include "DiscIO/VolumeVerifier.h"
+
+#include <algorithm>
+#include <cinttypes>
+#include <limits>
+#include <optional>
+#include <string>
+#include <unordered_set>
+
+#include "Common/Align.h"
+#include "Common/Assert.h"
+#include "Common/CommonTypes.h"
+#include "Common/MsgHandler.h"
+#include "Common/StringUtil.h"
+#include "Common/Swap.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/DiscExtractor.h"
+#include "DiscIO/Enums.h"
+#include "DiscIO/Filesystem.h"
+#include "DiscIO/Volume.h"
+#include "DiscIO/VolumeWii.h"
+
+namespace DiscIO
+{
+constexpr u64 MINI_DVD_SIZE = 1459978240; // GameCube
+constexpr u64 SL_DVD_SIZE = 4699979776; // Wii retail
+constexpr u64 SL_DVD_R_SIZE = 4707319808; // Wii RVT-R
+constexpr u64 DL_DVD_SIZE = 8511160320; // Wii retail
+constexpr u64 DL_DVD_R_SIZE = 8543666176; // Wii RVT-R
+
+constexpr u64 BLOCK_SIZE = 0x20000;
+
+VolumeVerifier::VolumeVerifier(const Volume& volume)
+ : m_volume(volume), m_started(false), m_done(false), m_progress(0),
+ m_max_progress(volume.GetSize())
+{
+}
+
+void VolumeVerifier::Start()
+{
+ ASSERT(!m_started);
+ m_started = true;
+
+ m_is_tgc = m_volume.GetBlobType() == BlobType::TGC;
+ m_is_datel = IsDisc(m_volume.GetVolumeType()) &&
+ !GetBootDOLOffset(m_volume, m_volume.GetGamePartition()).has_value();
+ m_is_not_retail =
+ (m_volume.GetVolumeType() == Platform::WiiDisc && !m_volume.IsEncryptedAndHashed()) ||
+ IsDebugSigned();
+
+ CheckPartitions();
+ if (m_volume.GetVolumeType() == Platform::WiiWAD)
+ CheckCorrectlySigned(PARTITION_NONE, GetStringT("This title is not correctly signed."));
+ CheckDiscSize();
+ CheckMisc();
+}
+
+void VolumeVerifier::CheckPartitions()
+{
+ const std::vector<Partition> partitions = m_volume.GetPartitions();
+ if (partitions.empty())
+ {
+ if (m_volume.GetVolumeType() != Platform::WiiWAD &&
+ !m_volume.GetFileSystem(m_volume.GetGamePartition()))
+ {
+ AddProblem(Severity::High, GetStringT("The filesystem is invalid or could not be read."));
+ }
+ return;
+ }
+
+ 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,
+ 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, GetStringT("The update partition is missing."));
+
+ if (std::find(types.cbegin(), types.cend(), PARTITION_DATA) == types.cend())
+ AddProblem(Severity::High, 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, 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, GetStringT("The install partition is missing."));
+
+ if (ShouldHaveMasterpiecePartitions() &&
+ types.cend() ==
+ std::find_if(types.cbegin(), types.cend(), [](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, GetStringT("The Masterpiece partitions are missing."));
+ }
+
+ for (const Partition& partition : partitions)
+ {
+ if (m_volume.GetPartitionType(partition) == PARTITION_UPDATE && partition.offset != 0x50000)
+ {
+ AddProblem(Severity::Low, GetStringT("The update partition is not at its normal position."));
+ }
+
+ const u64 normal_data_offset = m_volume.IsEncryptedAndHashed() ? 0xF800000 : 0x838000;
+ if (m_volume.GetPartitionType(partition) == PARTITION_DATA &&
+ partition.offset != normal_data_offset && !has_channel_partition && !has_install_partition)
+ {
+ AddProblem(
+ Severity::Low,
+ GetStringT("The data partition is not at its normal position. This will affect the "
+ "emulated loading times. When using NetPlay or sending input recordings to "
+ "other people, you will experience desyncs if anyone is using a good dump."));
+ }
+ }
+
+ for (const Partition& partition : partitions)
+ CheckPartition(partition);
+}
+
+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, 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;
+
+ 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 = StringFromFormat(GetStringT("%s (Masterpiece)").c_str(), name.c_str());
+ }
+
+ if (partition.offset % VolumeWii::BLOCK_TOTAL_SIZE != 0 ||
+ m_volume.PartitionOffsetToRawOffset(0, partition) % VolumeWii::BLOCK_TOTAL_SIZE != 0)
+ {
+ AddProblem(Severity::Medium,
+ StringFromFormat(GetStringT("The %s partition is not properly aligned.").c_str(),
+ name.c_str()));
+ }
+
+ CheckCorrectlySigned(
+ partition, StringFromFormat(GetStringT("The %s partition is not correctly signed.").c_str(),
+ name.c_str()));
+
+ bool invalid_disc_header = false;
+ std::vector<u8> disc_header(0x80);
+ constexpr u32 WII_MAGIC = 0x5D1C9EA3;
+ if (!m_volume.Read(0, disc_header.size(), disc_header.data(), partition))
+ {
+ invalid_disc_header = true;
+ }
+ else if (Common::swap32(disc_header.data() + 0x18) != WII_MAGIC)
+ {
+ for (size_t i = 0; i < disc_header.size(); i += 4)
+ {
+ if (Common::swap32(disc_header.data() + i) != i)
+ {
+ invalid_disc_header = true;
+ break;
+ }
+ }
+
+ // The loop above ends without breaking 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.
+ return false;
+ }
+ if (invalid_disc_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
+ const std::string text = StringFromFormat(
+ GetStringT("The %s partition does not seem to contain valid data.").c_str(), name.c_str());
+ AddProblem(severity, text);
+ return false;
+ }
+
+ const DiscIO::FileSystem* filesystem = m_volume.GetFileSystem(partition);
+ if (!filesystem)
+ {
+ const std::string text = StringFromFormat(
+ GetStringT("The %s partition does not have a valid file system.").c_str(), name.c_str());
+ AddProblem(severity, text);
+ return false;
+ }
+
+ if (type == PARTITION_UPDATE)
+ {
+ std::unique_ptr<FileInfo> file_info = filesystem->FindFileInfo("_sys");
+ bool has_correct_ios = false;
+ if (file_info)
+ {
+ const IOS::ES::TMDReader& tmd = m_volume.GetTMD(m_volume.GetGamePartition());
+ if (tmd.IsValid())
+ {
+ const std::string correct_ios = "IOS" + std::to_string(tmd.GetIOSId() & 0xFF) + "-";
+ for (const FileInfo& f : *file_info)
+ {
+ if (StringBeginsWith(f.GetName(), 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,
+ GetStringT("The update partition does not contain the IOS used by this title."));
+ }
+ }
+
+ return true;
+}
+
+void VolumeVerifier::CheckCorrectlySigned(const Partition& partition, const std::string& error_text)
+{
+ IOS::HLE::Kernel ios;
+ const auto es = ios.GetES();
+ const std::vector<u8> cert_chain = m_volume.GetCertificateChain(partition);
+
+ if (IOS::HLE::IPC_SUCCESS !=
+ es->VerifyContainer(IOS::HLE::Device::ES::VerifyContainerType::Ticket,
+ IOS::HLE::Device::ES::VerifyMode::DoNotUpdateCertStore,
+ m_volume.GetTicket(partition), cert_chain) ||
+ IOS::HLE::IPC_SUCCESS !=
+ es->VerifyContainer(IOS::HLE::Device::ES::VerifyContainerType::TMD,
+ IOS::HLE::Device::ES::VerifyMode::DoNotUpdateCertStore,
+ m_volume.GetTMD(partition), cert_chain))
+ {
+ AddProblem(Severity::Low, error_text);
+ }
+}
+
+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
+{
+ const std::unordered_set<std::string> channel_discs{
+ "RFNE01", "RFNJ01", "RFNK01", "RFNP01", "RFNW01", "RFPE01", "RFPJ01", "RFPK01", "RFPP01",
+ "RFPW01", "RGWE41", "RGWJ41", "RGWP41", "RGWX41", "RMCE01", "RMCJ01", "RMCK01", "RMCP01",
+ };
+
+ return channel_discs.find(m_volume.GetGameID()) != channel_discs.end();
+}
+
+bool VolumeVerifier::ShouldHaveInstallPartition() const
+{
+ const std::unordered_set<std::string> dragon_quest_x{"S4MJGD", "S4SJGD", "S6TJGD", "SDQJGD"};
+ return dragon_quest_x.find(m_volume.GetGameID()) != dragon_quest_x.end();
+}
+
+bool VolumeVerifier::ShouldHaveMasterpiecePartitions() const
+{
+ const std::unordered_set<std::string> ssbb{"RSBE01", "RSBJ01", "RSBK01", "RSBP01"};
+ return ssbb.find(m_volume.GetGameID()) != ssbb.end();
+}
+
+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.
+ const std::unordered_set<std::string> dual_layer_discs{
+ "R3ME01", "R3MP01", "R3OE01", "R3OJ01", "R3OP01", "RSBE01", "RSBJ01", "RSBK01", "RSBP01",
+ "RXMJ8P", "S59E01", "S59JC8", "S59P01", "S5QJC8", "SK8X52", "SAKENS", "SAKPNS", "SK8V52",
+ "SK8X52", "SLSEXJ", "SLSP01", "SQIE4Q", "SQIP4Q", "SQIY4Q", "SR5E41", "SR5P41", "SUOE41",
+ "SUOP41", "SVXX52", "SVXY52", "SX4E01", "SX4P01", "SZ3EGT", "SZ3PGT",
+ };
+
+ return dual_layer_discs.find(m_volume.GetGameID()) != dual_layer_discs.end();
+}
+
+void VolumeVerifier::CheckDiscSize()
+{
+ if (!IsDisc(m_volume.GetVolumeType()))
+ return;
+
+ const u64 biggest_offset = GetBiggestUsedOffset();
+ if (biggest_offset > m_volume.GetSize())
+ {
+ const bool second_layer_missing =
+ biggest_offset > SL_DVD_SIZE && m_volume.GetSize() >= SL_DVD_SIZE;
+ const std::string text =
+ second_layer_missing ?
+ 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.") :
+ 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, text);
+ return;
+ }
+
+ if (ShouldBeDualLayer() && biggest_offset <= SL_DVD_R_SIZE)
+ {
+ AddProblem(
+ Severity::Medium,
+ 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_volume.IsSizeAccurate())
+ {
+ AddProblem(Severity::Low, GetStringT("The format that the disc image is saved in does not "
+ "store the size of the disc image."));
+ }
+ else if (!m_is_tgc)
+ {
+ const Platform platform = m_volume.GetVolumeType();
+ const u64 size = m_volume.GetSize();
+
+ const bool valid_gamecube = size == MINI_DVD_SIZE;
+ const bool valid_retail_wii = size == SL_DVD_SIZE || size == DL_DVD_SIZE;
+ const bool valid_debug_wii = size == SL_DVD_R_SIZE || size == DL_DVD_R_SIZE;
+
+ const bool debug = IsDebugSigned();
+ if ((platform == Platform::GameCubeDisc && !valid_gamecube) ||
+ (platform == Platform::WiiDisc && (debug ? !valid_debug_wii : !valid_retail_wii)))
+ {
+ if (debug && valid_retail_wii)
+ {
+ AddProblem(Severity::Low,
+ GetStringT("This debug disc image has the size of a retail disc image."));
+ }
+ else
+ {
+ const bool small =
+ (m_volume.GetVolumeType() == Platform::GameCubeDisc && size < MINI_DVD_SIZE) ||
+ (m_volume.GetVolumeType() == Platform::WiiDisc && size < SL_DVD_SIZE);
+
+ if (small)
+ {
+ AddProblem(Severity::Low,
+ GetStringT("This disc image has an unusual size. This will likely make the "
+ "emulated loading times longer. When using NetPlay or sending "
+ "input recordings to other people, you will likely experience "
+ "desyncs if anyone is using a good dump."));
+ }
+ else
+ {
+ AddProblem(Severity::Low, GetStringT("This disc image has an unusual size."));
+ }
+ }
+ }
+ }
+}
+
+u64 VolumeVerifier::GetBiggestUsedOffset()
+{
+ std::vector<Partition> partitions = m_volume.GetPartitions();
+ if (partitions.empty())
+ partitions.emplace_back(m_volume.GetGamePartition());
+
+ const u64 disc_header_size = m_volume.GetVolumeType() == Platform::GameCubeDisc ? 0x460 : 0x50000;
+ u64 biggest_offset = disc_header_size;
+ for (const Partition& partition : partitions)
+ {
+ if (partition != PARTITION_NONE)
+ {
+ const u64 offset = m_volume.PartitionOffsetToRawOffset(0x440, partition);
+ biggest_offset = std::max(biggest_offset, offset);
+ }
+
+ const std::optional<u64> dol_offset = GetBootDOLOffset(m_volume, partition);
+ if (dol_offset)
+ {
+ const std::optional<u64> dol_size = GetBootDOLSize(m_volume, partition, *dol_offset);
+ if (dol_size)
+ {
+ const u64 offset = m_volume.PartitionOffsetToRawOffset(*dol_offset + *dol_size, partition);
+ biggest_offset = std::max(biggest_offset, offset);
+ }
+ }
+
+ const std::optional<u64> fst_offset = GetFSTOffset(m_volume, partition);
+ const std::optional<u64> fst_size = GetFSTSize(m_volume, partition);
+ if (fst_offset && fst_size)
+ {
+ const u64 offset = m_volume.PartitionOffsetToRawOffset(*fst_offset + *fst_size, partition);
+ biggest_offset = std::max(biggest_offset, offset);
+ }
+
+ const FileSystem* fs = m_volume.GetFileSystem(partition);
+ if (fs)
+ {
+ const u64 offset =
+ m_volume.PartitionOffsetToRawOffset(GetBiggestUsedOffset(fs->GetRoot()), partition);
+ biggest_offset = std::max(biggest_offset, offset);
+ }
+ }
+ return biggest_offset;
+}
+
+u64 VolumeVerifier::GetBiggestUsedOffset(const FileInfo& file_info) const
+{
+ if (file_info.IsDirectory())
+ {
+ u64 biggest_offset = 0;
+ for (const FileInfo& f : file_info)
+ biggest_offset = std::max(biggest_offset, GetBiggestUsedOffset(f));
+ return biggest_offset;
+ }
+ else
+ {
+ return file_info.GetOffset() + file_info.GetSize();
+ }
+}
+
+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 (StringBeginsWith(game_id_unencrypted, "410"))
+ {
+ // This is the Wii Backup Disc (aka "pinkfish" disc),
+ // which legitimately has an inconsistent game ID.
+ inconsistent_game_id = false;
+ }
+ else if (StringBeginsWith(game_id_unencrypted, "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,
+ StringFromFormat(GetStringT("The game ID is %s but should be %s.").c_str(),
+ game_id_unencrypted.c_str(), proper_game_id.c_str()));
+ inconsistent_game_id = false;
+ }
+ }
+
+ if (inconsistent_game_id)
+ {
+ AddProblem(Severity::Low, GetStringT("The game ID is inconsistent."));
+ }
+ }
+
+ const Region region = m_volume.GetRegion();
+ const Platform platform = m_volume.GetVolumeType();
+
+ if (game_id_encrypted.size() < 4)
+ {
+ AddProblem(Severity::Low, GetStringT("The game ID is unusually short."));
+ }
+ else
+ {
+ const char country_code = game_id_encrypted[3];
+ if (CountryCodeToRegion(country_code, platform, region) != region)
+ {
+ AddProblem(
+ Severity::Medium,
+ 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 pirated Korean games that have had 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 to have 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.
+ 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 also intended to catch fakesigned pirated Korean games,
+ // but this time with the IOS slot set to cIOS instead of IOS36.
+ AddProblem(Severity::High, GetStringT("This title is set to use an invalid IOS."));
+ }
+ }
+
+ const IOS::ES::TicketReader& ticket = m_volume.GetTicket(m_volume.GetGamePartition());
+ if (ticket.IsValid())
+ {
+ const u8 common_key = ticket.GetCommonKeyIndex();
+
+ if (common_key > 1)
+ {
+ // Many fakesigned WADs have the common key index set to a (random?) bogus value.
+ // For WADs, Dolphin will detect this and use common key 0 instead, making this low severity.
+ const Severity severity =
+ m_volume.GetVolumeType() == Platform::WiiWAD ? Severity::Low : Severity::High;
+ // i18n: This is "common" as in "shared", not the opposite of "uncommon"
+ AddProblem(Severity::Low, GetStringT("This title is set to use an invalid common key."));
+ }
+
+ if (common_key == 1 && region != Region::NTSC_K)
+ {
+ // Apparently a certain pirate WAD of Chronos Twins DX unluckily got an index of 1,
+ // which Dolphin does not change to 0 because 1 is valid on Korean Wiis.
+ // https://forums.dolphin-emu.org/Thread-wiiware-chronos-twins-dx
+ AddProblem(Severity::High,
+ // i18n: This is "common" as in "shared", not the opposite of "uncommon"
+ GetStringT("This non-Korean title is set to use the Korean common key."));
+ }
+ }
+
+ if (IsDisc(m_volume.GetVolumeType()))
+ {
+ constexpr u32 NKIT_MAGIC = 0x4E4B4954; // "NKIT"
+ if (m_volume.ReadSwapped<u32>(0x200, PARTITION_NONE) == NKIT_MAGIC)
+ {
+ AddProblem(Severity::Low,
+ 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."));
+ }
+ }
+}
+
+void VolumeVerifier::Process()
+{
+ ASSERT(m_started);
+ ASSERT(!m_done);
+
+ if (m_progress == m_max_progress)
+ return;
+
+ m_progress += std::min(m_max_progress - m_progress, BLOCK_SIZE);
+}
+
+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;
+
+ // 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_is_datel)
+ {
+ m_result.summary_text = GetStringT("Dolphin is unable to verify unlicensed discs.");
+ return;
+ }
+
+ if (m_is_tgc)
+ {
+ m_result.summary_text = GetStringT("Dolphin is unable to verify typical TGC files properly, "
+ "since they are not dumps of actual discs.");
+ return;
+ }
+
+ switch (highest_severity)
+ {
+ case Severity::None:
+ if (IsWii(m_volume.GetVolumeType()) && !m_is_not_retail)
+ {
+ m_result.summary_text =
+ 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 = GetStringT("No problems were found.");
+ }
+ break;
+ case Severity::Low:
+ m_result.summary_text = 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 = 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 = 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 +=
+ 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 += GetStringT("\n\nBecause this title is not for retail Wii consoles, "
+ "Dolphin cannot verify that it hasn't been tampered with.");
+ }
+}
+
+const VolumeVerifier::Result& VolumeVerifier::GetResult() const
+{
+ return m_result;
+}
+
+void VolumeVerifier::AddProblem(Severity severity, const std::string& text)
+{
+ m_result.problems.emplace_back(Problem{severity, text});
+}
+
+} // namespace DiscIO