From 84cbd5150f62bd5dd4a8d621fd0ccf7623ca4e2c Mon Sep 17 00:00:00 2001 From: JosJuice Date: Thu, 21 Mar 2019 23:04:56 +0100 Subject: Add a Verify tab to game properties --- Source/Core/DiscIO/VolumeVerifier.cpp | 689 ++++++++++++++++++++++++++++++++++ 1 file changed, 689 insertions(+) create mode 100644 Source/Core/DiscIO/VolumeVerifier.cpp (limited to 'Source/Core/DiscIO/VolumeVerifier.cpp') 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 +#include +#include +#include +#include +#include + +#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 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 partitions_in_first_table = m_volume.ReadSwapped(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 types; + for (const Partition& partition : partitions) + { + const std::optional 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 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 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 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 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 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 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 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 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 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 dol_offset = GetBootDOLOffset(m_volume, partition); + if (dol_offset) + { + const std::optional 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 fst_offset = GetFSTOffset(m_volume, partition); + const std::optional 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(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 -- cgit v1.2.3 From 4fd2d8e8c4db14cf01576ab228964b80c8ec37ad Mon Sep 17 00:00:00 2001 From: JosJuice Date: Tue, 26 Mar 2019 17:22:18 +0100 Subject: VolumeVerifier: Check hashes in Wii partitions --- Source/Core/DiscIO/VolumeVerifier.cpp | 95 ++++++++++++++++++++++++++++++----- 1 file changed, 82 insertions(+), 13 deletions(-) (limited to 'Source/Core/DiscIO/VolumeVerifier.cpp') diff --git a/Source/Core/DiscIO/VolumeVerifier.cpp b/Source/Core/DiscIO/VolumeVerifier.cpp index 7d3eba4c8f..2201784d1c 100644 --- a/Source/Core/DiscIO/VolumeVerifier.cpp +++ b/Source/Core/DiscIO/VolumeVerifier.cpp @@ -14,6 +14,7 @@ #include "Common/Align.h" #include "Common/Assert.h" #include "Common/CommonTypes.h" +#include "Common/Logging/Log.h" #include "Common/MsgHandler.h" #include "Common/StringUtil.h" #include "Common/Swap.h" @@ -62,6 +63,9 @@ void VolumeVerifier::Start() CheckCorrectlySigned(PARTITION_NONE, GetStringT("This title is not correctly signed.")); CheckDiscSize(); CheckMisc(); + + std::sort(m_blocks.begin(), m_blocks.end(), + [](const BlockToVerify& b1, const BlockToVerify& b2) { return b1.offset < b2.offset; }); } void VolumeVerifier::CheckPartitions() @@ -164,18 +168,7 @@ bool VolumeVerifier::CheckPartition(const Partition& partition) 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()); - } + const std::string name = GetPartitionName(type); if (partition.offset % VolumeWii::BLOCK_TOTAL_SIZE != 0 || m_volume.PartitionOffsetToRawOffset(0, partition) % VolumeWii::BLOCK_TOTAL_SIZE != 0) @@ -189,6 +182,13 @@ bool VolumeVerifier::CheckPartition(const Partition& partition) partition, StringFromFormat(GetStringT("The %s partition is not correctly signed.").c_str(), name.c_str())); + if (m_volume.SupportsIntegrityCheck() && !m_volume.CheckH3TableIntegrity(partition)) + { + const std::string text = StringFromFormat( + GetStringT("The H3 hash table for the %s partition is not correct.").c_str(), name.c_str()); + AddProblem(Severity::Low, text); + } + bool invalid_disc_header = false; std::vector disc_header(0x80); constexpr u32 WII_MAGIC = 0x5D1C9EA3; @@ -262,9 +262,43 @@ bool VolumeVerifier::CheckPartition(const Partition& partition) } } + // Prepare for hash verification in the Process step + if (m_volume.SupportsIntegrityCheck()) + { + u64 offset = m_volume.PartitionOffsetToRawOffset(0, partition); + const std::optional size = + m_volume.ReadSwappedAndShifted(partition.offset + 0x2bc, PARTITION_NONE); + const u64 end_offset = offset + size.value_or(0); + + for (size_t i = 0; offset < end_offset; ++i, offset += VolumeWii::BLOCK_TOTAL_SIZE) + m_blocks.emplace_back(BlockToVerify{partition, offset, i}); + + m_block_errors.emplace(partition, 0); + } + return true; } +std::string VolumeVerifier::GetPartitionName(std::optional 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 = StringFromFormat(GetStringT("%s (Masterpiece)").c_str(), name.c_str()); + } + return name; +} + void VolumeVerifier::CheckCorrectlySigned(const Partition& partition, const std::string& error_text) { IOS::HLE::Kernel ios; @@ -596,7 +630,30 @@ void VolumeVerifier::Process() if (m_progress == m_max_progress) return; - m_progress += std::min(m_max_progress - m_progress, BLOCK_SIZE); + u64 bytes_to_read = BLOCK_SIZE; + if (m_block_index < m_blocks.size() && m_blocks[m_block_index].offset == m_progress) + { + bytes_to_read = VolumeWii::BLOCK_TOTAL_SIZE; + } + else if (m_block_index + 1 < m_blocks.size() && m_blocks[m_block_index + 1].offset > m_progress) + { + bytes_to_read = std::min(bytes_to_read, m_blocks[m_block_index + 1].offset - m_progress); + } + bytes_to_read = std::min(bytes_to_read, m_max_progress - m_progress); + + m_progress += bytes_to_read; + + while (m_block_index < m_blocks.size() && m_blocks[m_block_index].offset < m_progress) + { + if (!m_volume.CheckBlockIntegrity(m_blocks[m_block_index].block_index, + m_blocks[m_block_index].partition)) + { + WARN_LOG(DISCIO, "Integrity check failed for block at 0x%" PRIx64, + m_blocks[m_block_index].offset); + m_block_errors[m_blocks[m_block_index].partition]++; + } + m_block_index++; + } } u64 VolumeVerifier::GetBytesProcessed() const @@ -615,6 +672,18 @@ void VolumeVerifier::Finish() return; m_done = true; + for (auto pair : m_block_errors) + { + if (pair.second > 0) + { + const std::string name = GetPartitionName(m_volume.GetPartitionType(pair.first)); + AddProblem(Severity::Medium, + StringFromFormat( + GetStringT("Errors were found in %zu blocks in the %s partition.").c_str(), + pair.second, name.c_str())); + } + } + // 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; }); -- cgit v1.2.3 From eced9d7c7e2b05df304e800e4cfb7eae3ee9d2bd Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sat, 30 Mar 2019 12:45:00 +0100 Subject: VolumeVerifier: Calculate CRC32/MD5/SHA-1 --- Source/Core/DiscIO/VolumeVerifier.cpp | 83 ++++++++++++++++++++++++++++++++--- 1 file changed, 78 insertions(+), 5 deletions(-) (limited to 'Source/Core/DiscIO/VolumeVerifier.cpp') diff --git a/Source/Core/DiscIO/VolumeVerifier.cpp b/Source/Core/DiscIO/VolumeVerifier.cpp index 2201784d1c..ac2f06cc15 100644 --- a/Source/Core/DiscIO/VolumeVerifier.cpp +++ b/Source/Core/DiscIO/VolumeVerifier.cpp @@ -11,6 +11,10 @@ #include #include +#include +#include +#include + #include "Common/Align.h" #include "Common/Assert.h" #include "Common/CommonTypes.h" @@ -40,9 +44,11 @@ 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()) +VolumeVerifier::VolumeVerifier(const Volume& volume, Hashes hashes_to_calculate) + : m_volume(volume), m_hashes_to_calculate(hashes_to_calculate), + m_calculating_any_hash(hashes_to_calculate.crc32 || hashes_to_calculate.md5 || + hashes_to_calculate.sha1), + m_started(false), m_done(false), m_progress(0), m_max_progress(volume.GetSize()) { } @@ -64,8 +70,7 @@ void VolumeVerifier::Start() CheckDiscSize(); CheckMisc(); - std::sort(m_blocks.begin(), m_blocks.end(), - [](const BlockToVerify& b1, const BlockToVerify& b2) { return b1.offset < b2.offset; }); + SetUpHashing(); } void VolumeVerifier::CheckPartitions() @@ -622,6 +627,27 @@ void VolumeVerifier::CheckMisc() } } +void VolumeVerifier::SetUpHashing() +{ + std::sort(m_blocks.begin(), m_blocks.end(), + [](const BlockToVerify& b1, const BlockToVerify& b2) { return b1.offset < b2.offset; }); + + if (m_hashes_to_calculate.crc32) + m_crc32_context = crc32(0, nullptr, 0); + + if (m_hashes_to_calculate.md5) + { + mbedtls_md5_init(&m_md5_context); + mbedtls_md5_starts(&m_md5_context); + } + + if (m_hashes_to_calculate.sha1) + { + mbedtls_sha1_init(&m_sha1_context); + mbedtls_sha1_starts(&m_sha1_context); + } +} + void VolumeVerifier::Process() { ASSERT(m_started); @@ -641,6 +667,30 @@ void VolumeVerifier::Process() } bytes_to_read = std::min(bytes_to_read, m_max_progress - m_progress); + if (m_calculating_any_hash) + { + std::vector data(bytes_to_read); + if (!m_volume.Read(m_progress, bytes_to_read, data.data(), PARTITION_NONE)) + { + m_calculating_any_hash = false; + } + else + { + if (m_hashes_to_calculate.crc32) + { + // It would be nice to use crc32_z here instead of crc32, but it isn't available on Android + m_crc32_context = + crc32(m_crc32_context, data.data(), static_cast(bytes_to_read)); + } + + if (m_hashes_to_calculate.md5) + mbedtls_md5_update(&m_md5_context, data.data(), bytes_to_read); + + if (m_hashes_to_calculate.sha1) + mbedtls_sha1_update(&m_sha1_context, data.data(), bytes_to_read); + } + } + m_progress += bytes_to_read; while (m_block_index < m_blocks.size() && m_blocks[m_block_index].offset < m_progress) @@ -672,6 +722,29 @@ void VolumeVerifier::Finish() return; m_done = true; + if (m_calculating_any_hash) + { + if (m_hashes_to_calculate.crc32) + { + m_result.hashes.crc32 = std::vector(4); + const u32 crc32_be = Common::swap32(m_crc32_context); + const u8* crc32_be_ptr = reinterpret_cast(&crc32_be); + std::copy(crc32_be_ptr, crc32_be_ptr + 4, m_result.hashes.crc32.begin()); + } + + if (m_hashes_to_calculate.md5) + { + m_result.hashes.md5 = std::vector(16); + mbedtls_md5_finish(&m_md5_context, m_result.hashes.md5.data()); + } + + if (m_hashes_to_calculate.sha1) + { + m_result.hashes.sha1 = std::vector(20); + mbedtls_sha1_finish(&m_sha1_context, m_result.hashes.sha1.data()); + } + } + for (auto pair : m_block_errors) { if (pair.second > 0) -- cgit v1.2.3 From a469fb315037d455884f25acb39bad57bc76e4ff Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sat, 30 Mar 2019 16:20:45 +0100 Subject: VolumeVerifier: Check whether invalid blocks are unused --- Source/Core/DiscIO/VolumeVerifier.cpp | 41 +++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 7 deletions(-) (limited to 'Source/Core/DiscIO/VolumeVerifier.cpp') diff --git a/Source/Core/DiscIO/VolumeVerifier.cpp b/Source/Core/DiscIO/VolumeVerifier.cpp index ac2f06cc15..74612656fa 100644 --- a/Source/Core/DiscIO/VolumeVerifier.cpp +++ b/Source/Core/DiscIO/VolumeVerifier.cpp @@ -29,6 +29,7 @@ #include "Core/IOS/IOSC.h" #include "DiscIO/Blob.h" #include "DiscIO/DiscExtractor.h" +#include "DiscIO/DiscScrubber.h" #include "DiscIO/Enums.h" #include "DiscIO/Filesystem.h" #include "DiscIO/Volume.h" @@ -629,6 +630,12 @@ void VolumeVerifier::CheckMisc() void VolumeVerifier::SetUpHashing() { + if (m_volume.GetVolumeType() == Platform::WiiDisc) + { + // Set up a DiscScrubber for checking whether blocks with errors are unused + m_scrubber.SetupScrub(&m_volume, VolumeWii::BLOCK_TOTAL_SIZE); + } + std::sort(m_blocks.begin(), m_blocks.end(), [](const BlockToVerify& b1, const BlockToVerify& b2) { return b1.offset < b2.offset; }); @@ -698,9 +705,17 @@ void VolumeVerifier::Process() if (!m_volume.CheckBlockIntegrity(m_blocks[m_block_index].block_index, m_blocks[m_block_index].partition)) { - WARN_LOG(DISCIO, "Integrity check failed for block at 0x%" PRIx64, - m_blocks[m_block_index].offset); - m_block_errors[m_blocks[m_block_index].partition]++; + const u64 offset = m_blocks[m_block_index].offset; + if (m_scrubber.CanBlockBeScrubbed(offset)) + { + WARN_LOG(DISCIO, "Integrity check failed for unused block at 0x%" PRIx64, offset); + m_unused_block_errors[m_blocks[m_block_index].partition]++; + } + else + { + WARN_LOG(DISCIO, "Integrity check failed for block at 0x%" PRIx64, offset); + m_block_errors[m_blocks[m_block_index].partition]++; + } } m_block_index++; } @@ -750,10 +765,22 @@ void VolumeVerifier::Finish() if (pair.second > 0) { const std::string name = GetPartitionName(m_volume.GetPartitionType(pair.first)); - AddProblem(Severity::Medium, - StringFromFormat( - GetStringT("Errors were found in %zu blocks in the %s partition.").c_str(), - pair.second, name.c_str())); + const std::string text = StringFromFormat( + GetStringT("Errors were found in %zu blocks in the %s partition.").c_str(), pair.second, + name.c_str()); + AddProblem(Severity::Medium, text); + } + } + + for (auto pair : m_unused_block_errors) + { + if (pair.second > 0) + { + const std::string name = GetPartitionName(m_volume.GetPartitionType(pair.first)); + const std::string text = StringFromFormat( + GetStringT("Errors were found in %zu unused blocks in the %s partition.").c_str(), + pair.second, name.c_str()); + AddProblem(Severity::Low, text); } } -- cgit v1.2.3 From 8709b21ac30f5da5c32ef056b6562a37345b5f82 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sat, 30 Mar 2019 17:42:31 +0100 Subject: VolumeVerifier: Verify WAD contents --- Source/Core/DiscIO/VolumeVerifier.cpp | 62 ++++++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 4 deletions(-) (limited to 'Source/Core/DiscIO/VolumeVerifier.cpp') diff --git a/Source/Core/DiscIO/VolumeVerifier.cpp b/Source/Core/DiscIO/VolumeVerifier.cpp index 74612656fa..6b70382044 100644 --- a/Source/Core/DiscIO/VolumeVerifier.cpp +++ b/Source/Core/DiscIO/VolumeVerifier.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -630,7 +631,11 @@ void VolumeVerifier::CheckMisc() void VolumeVerifier::SetUpHashing() { - if (m_volume.GetVolumeType() == Platform::WiiDisc) + 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, VolumeWii::BLOCK_TOTAL_SIZE); @@ -663,14 +668,28 @@ void VolumeVerifier::Process() if (m_progress == m_max_progress) return; + IOS::ES::Content content; + bool content_read = false; u64 bytes_to_read = BLOCK_SIZE; - if (m_block_index < m_blocks.size() && m_blocks[m_block_index].offset == m_progress) + 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; + } + 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_block_index < m_blocks.size() && m_blocks[m_block_index].offset == m_progress) { bytes_to_read = VolumeWii::BLOCK_TOTAL_SIZE; } - else if (m_block_index + 1 < m_blocks.size() && m_blocks[m_block_index + 1].offset > m_progress) + else if (m_block_index < m_blocks.size() && m_blocks[m_block_index].offset > m_progress) { - bytes_to_read = std::min(bytes_to_read, m_blocks[m_block_index + 1].offset - m_progress); + bytes_to_read = std::min(bytes_to_read, m_blocks[m_block_index].offset - m_progress); } bytes_to_read = std::min(bytes_to_read, m_max_progress - m_progress); @@ -700,6 +719,17 @@ void VolumeVerifier::Process() m_progress += bytes_to_read; + if (content_read) + { + if (!CheckContentIntegrity(content)) + { + AddProblem(Severity::High, + StringFromFormat(GetStringT("Content %08x is corrupt.").c_str(), content.id)); + } + + m_content_index++; + } + while (m_block_index < m_blocks.size() && m_blocks[m_block_index].offset < m_progress) { if (!m_volume.CheckBlockIntegrity(m_blocks[m_block_index].block_index, @@ -721,6 +751,30 @@ void VolumeVerifier::Process() } } +bool VolumeVerifier::CheckContentIntegrity(const IOS::ES::Content& content) +{ + const u64 padded_size = Common::AlignUp(content.size, 0x40); + std::vector encrypted_data(padded_size); + m_volume.Read(m_content_offsets[m_content_index], padded_size, encrypted_data.data(), + PARTITION_NONE); + + mbedtls_aes_context context; + const std::array key = m_volume.GetTicket(PARTITION_NONE).GetTitleKey(); + mbedtls_aes_setkey_dec(&context, key.data(), 128); + + std::array iv{}; + iv[0] = static_cast(content.index >> 8); + iv[1] = static_cast(content.index & 0xFF); + + std::vector decrypted_data(padded_size); + mbedtls_aes_crypt_cbc(&context, MBEDTLS_AES_DECRYPT, padded_size, iv.data(), + encrypted_data.data(), decrypted_data.data()); + + std::array sha1; + mbedtls_sha1(decrypted_data.data(), content.size, sha1.data()); + return sha1 == content.sha1; +} + u64 VolumeVerifier::GetBytesProcessed() const { return m_progress; -- cgit v1.2.3