@@ -145,7 +145,8 @@ Country TypicalCountryForRegion(Region region)
}
}

Region CountryCodeToRegion(u8 country_code, Platform platform, Region expected_region)
Region CountryCodeToRegion(u8 country_code, Platform platform, Region expected_region,
std::optional<u16> revision)
{
switch (country_code)
{
@@ -159,11 +160,24 @@ Region CountryCodeToRegion(u8 country_code, Platform platform, Region expected_r
return Region::NTSC_J; // Korean GC games in English or Taiwanese Wii games

case 'E':
if (expected_region == Region::NTSC_J)
return Region::NTSC_J; // Korean GC games in English
else
if (platform != Platform::GameCubeDisc)
return Region::NTSC_U; // The most common country code for NTSC-U

if (revision)
{
if (*revision >= 0x30)
return Region::NTSC_J; // Korean GC games in English
else
return Region::NTSC_U; // The most common country code for NTSC-U
}
else
{
if (expected_region == Region::NTSC_J)
return Region::NTSC_J; // Korean GC games in English
else
return Region::NTSC_U; // The most common country code for NTSC-U
}

case 'B':
case 'N':
return Region::NTSC_U;
@@ -198,7 +212,8 @@ Region CountryCodeToRegion(u8 country_code, Platform platform, Region expected_r
}
}

Country CountryCodeToCountry(u8 country_code, Platform platform, Region region)
Country CountryCodeToCountry(u8 country_code, Platform platform, Region region,
std::optional<u16> revision)
{
switch (country_code)
{
@@ -214,10 +229,10 @@ Country CountryCodeToCountry(u8 country_code, Platform platform, Region region)
return region == Region::NTSC_U ? Country::USA : Country::Europe;

case 'W':
if (region == Region::PAL)
return Country::Europe; // Only the Nordic version of Ratatouille (Wii)
else if (platform == Platform::GameCubeDisc)
if (platform == Platform::GameCubeDisc)
return Country::Korea; // GC games in English released in Korea
else if (region == Region::PAL)
return Country::Europe; // Only the Nordic version of Ratatouille (Wii)
else
return Country::Taiwan; // Wii games in traditional Chinese released in Taiwan

@@ -251,11 +266,24 @@ Country CountryCodeToCountry(u8 country_code, Platform platform, Region region)

// NTSC
case 'E':
if (region == Region::NTSC_J)
return Country::Korea; // GC games in English released in Korea
else
if (platform != Platform::GameCubeDisc)
return Country::USA; // The most common country code for NTSC-U

if (revision)
{
if (*revision >= 0x30)
return Country::Korea; // GC games in English released in Korea
else
return Country::USA; // The most common country code for NTSC-U
}
else
{
if (region == Region::NTSC_J)
return Country::Korea; // GC games in English released in Korea
else
return Country::USA; // The most common country code for NTSC-U
}

case 'B': // PAL games released on NTSC-U VC
case 'N': // NTSC-J games released on NTSC-U VC
return Country::USA;
@@ -4,6 +4,7 @@

#pragma once

#include <optional>
#include <string>

#include "Common/CommonTypes.h"
@@ -78,8 +79,10 @@ bool IsNTSC(Region region);
Country TypicalCountryForRegion(Region region);
// Avoid using this function if you can. Country codes aren't always reliable region indicators.
Region CountryCodeToRegion(u8 country_code, Platform platform,
Region expected_region = Region::Unknown);
Country CountryCodeToCountry(u8 country_code, Platform platform, Region region = Region::Unknown);
Region expected_region = Region::Unknown,
std::optional<u16> revision = {});
Country CountryCodeToCountry(u8 country_code, Platform platform, Region region = Region::Unknown,
std::optional<u16> revision = {});

Region GetSysMenuRegion(u16 title_version);
std::string GetSysMenuVersionString(u16 title_version);
@@ -20,8 +20,9 @@ class PlainFileReader : public BlobReader
static std::unique_ptr<PlainFileReader> Create(File::IOFile file);

BlobType GetBlobType() const override { return BlobType::PLAIN; }
u64 GetDataSize() const override { return m_size; }
u64 GetRawSize() const override { return m_size; }
u64 GetDataSize() const override { return m_size; }
bool IsDataSizeAccurate() const override { return true; }
bool Read(u64 offset, u64 nbytes, u8* out_ptr) override;

private:
@@ -43,8 +43,9 @@ class TGCFileReader final : public BlobReader
static std::unique_ptr<TGCFileReader> Create(File::IOFile file);

BlobType GetBlobType() const override { return BlobType::TGC; }
u64 GetDataSize() const override;
u64 GetRawSize() const override { return m_size; }
u64 GetDataSize() const override;
bool IsDataSizeAccurate() const override { return true; }
bool Read(u64 offset, u64 nbytes, u8* out_ptr) override;

private:
@@ -25,6 +25,7 @@ namespace DiscIO
{
const IOS::ES::TicketReader Volume::INVALID_TICKET{};
const IOS::ES::TMDReader Volume::INVALID_TMD{};
const std::vector<u8> Volume::INVALID_CERT_CHAIN{};

std::map<Language, std::string> Volume::ReadWiiNames(const std::vector<char16_t>& data)
{
@@ -69,6 +69,11 @@ class Volume
return INVALID_TICKET;
}
virtual const IOS::ES::TMDReader& GetTMD(const Partition& partition) const { return INVALID_TMD; }
virtual const std::vector<u8>& GetCertificateChain(const Partition& partition) const
{
return INVALID_CERT_CHAIN;
}
virtual std::vector<u64> GetContentOffsets() const { return {}; }
// Returns a non-owning pointer. Returns nullptr if the file system couldn't be read.
virtual const FileSystem* GetFileSystem(const Partition& partition) const = 0;
virtual u64 PartitionOffsetToRawOffset(u64 offset, const Partition& partition) const
@@ -95,12 +100,17 @@ class Volume
}
virtual Platform GetVolumeType() const = 0;
virtual bool SupportsIntegrityCheck() const { return false; }
virtual bool CheckIntegrity(const Partition& partition) const { return false; }
virtual bool CheckH3TableIntegrity(const Partition& partition) const { return false; }
virtual bool CheckBlockIntegrity(u64 block_index, const Partition& partition) const
{
return false;
}
virtual Region GetRegion() const = 0;
virtual Country GetCountry(const Partition& partition = PARTITION_NONE) const = 0;
virtual BlobType GetBlobType() const = 0;
// Size of virtual disc (may be inaccurate depending on the blob type)
virtual u64 GetSize() const = 0;
virtual bool IsSizeAccurate() const = 0;
// Size on disc (compressed size)
virtual u64 GetRawSize() const = 0;

@@ -128,8 +138,9 @@ class Volume

static const IOS::ES::TicketReader INVALID_TICKET;
static const IOS::ES::TMDReader INVALID_TMD;
static const std::vector<u8> INVALID_CERT_CHAIN;
};

std::unique_ptr<Volume> CreateVolumeFromFilename(const std::string& filename);

} // namespace
} // namespace DiscIO
@@ -23,8 +23,9 @@ class VolumeFileBlobReader final : public BlobReader
Create(const Volume& volume, const Partition& partition, const std::string& file_path);

BlobType GetBlobType() const override { return BlobType::PLAIN; }
u64 GetDataSize() const override;
u64 GetRawSize() const override;
u64 GetDataSize() const override;
bool IsDataSizeAccurate() const override { return true; }
bool Read(u64 offset, u64 length, u8* out_ptr) override;

private:
@@ -97,11 +97,12 @@ Country VolumeGC::GetCountry(const Partition& partition) const
// The 0 that we use as a default value is mapped to Country::Unknown and Region::Unknown
const u8 country = ReadSwapped<u8>(3, partition).value_or(0);
const Region region = GetRegion();
const std::optional<u16> revision = GetRevision();

if (CountryCodeToRegion(country, Platform::GameCubeDisc, region) != region)
if (CountryCodeToRegion(country, Platform::GameCubeDisc, region, revision) != region)
return TypicalCountryForRegion(region);

return CountryCodeToCountry(country, Platform::GameCubeDisc, region);
return CountryCodeToCountry(country, Platform::GameCubeDisc, region, revision);
}

std::string VolumeGC::GetMakerID(const Partition& partition) const
@@ -179,6 +180,11 @@ u64 VolumeGC::GetSize() const
return m_reader->GetDataSize();
}

bool VolumeGC::IsSizeAccurate() const
{
return m_reader->IsDataSizeAccurate();
}

u64 VolumeGC::GetRawSize() const
{
return m_reader->GetRawSize();
@@ -52,6 +52,7 @@ class VolumeGC : public Volume
Country GetCountry(const Partition& partition = PARTITION_NONE) const override;
BlobType GetBlobType() const override;
u64 GetSize() const override;
bool IsSizeAccurate() const override;
u64 GetRawSize() const override;

private:

Large diffs are not rendered by default.

@@ -0,0 +1,134 @@
// Copyright 2019 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.

#pragma once

#include <map>
#include <optional>
#include <string>
#include <vector>

#include <mbedtls/md5.h>
#include <mbedtls/sha1.h>

#include "Common/CommonTypes.h"
#include "DiscIO/DiscScrubber.h"
#include "DiscIO/Volume.h"

// To be used as follows:
//
// VolumeVerifier verifier(volume);
// verifier.Start();
// while (verifier.GetBytesProcessed() != verifier.GetTotalBytes())
// verifier.Process();
// verifier.Finish();
// auto result = verifier.GetResult();
//
// Start, Process and Finish may take some time to run.
//
// GetResult() can be called before the processing is finished, but the result will be incomplete.

namespace IOS::ES
{
struct Content;
class SignedBlobReader;
}

namespace DiscIO
{
class FileInfo;

class VolumeVerifier final
{
public:
enum class Severity
{
None, // Only used internally
Low,
Medium,
High,
};

struct Problem
{
Severity severity;
std::string text;
};

template <typename T>
struct Hashes
{
T crc32;
T md5;
T sha1;
};

struct Result
{
Hashes<std::vector<u8>> hashes;
std::string summary_text;
std::vector<Problem> problems;
};

VolumeVerifier(const Volume& volume, Hashes<bool> hashes_to_calculate);
void Start();
void Process();
u64 GetBytesProcessed() const;
u64 GetTotalBytes() const;
void Finish();
const Result& GetResult() const;

private:
struct BlockToVerify
{
Partition partition;
u64 offset;
u64 block_index;
};

void CheckPartitions();
bool CheckPartition(const Partition& partition); // Returns false if partition should be ignored
std::string GetPartitionName(std::optional<u32> type) const;
void CheckCorrectlySigned(const Partition& partition, const std::string& error_text);
bool IsDebugSigned() const;
bool ShouldHaveChannelPartition() const;
bool ShouldHaveInstallPartition() const;
bool ShouldHaveMasterpiecePartitions() const;
bool ShouldBeDualLayer() const;
void CheckDiscSize();
u64 GetBiggestUsedOffset();
u64 GetBiggestUsedOffset(const FileInfo& file_info) const;
void CheckMisc();
void SetUpHashing();
bool CheckContentIntegrity(const IOS::ES::Content& content);

void AddProblem(Severity severity, const std::string& text);

const Volume& m_volume;
Result m_result;
bool m_is_tgc;
bool m_is_datel;
bool m_is_not_retail;

Hashes<bool> m_hashes_to_calculate;
bool m_calculating_any_hash;
unsigned long m_crc32_context;
mbedtls_md5_context m_md5_context;
mbedtls_sha1_context m_sha1_context;

DiscScrubber m_scrubber;
std::vector<u64> m_content_offsets;
u16 m_content_index = 0;
std::vector<BlockToVerify> m_blocks;
size_t m_block_index = 0; // Index in m_blocks, not index in a specific partition
std::map<Partition, size_t> m_block_errors;
std::map<Partition, size_t> m_unused_block_errors;

bool m_started;
bool m_done;
u64 m_progress;
u64 m_max_progress;
};

} // namespace DiscIO
@@ -32,16 +32,20 @@ VolumeWAD::VolumeWAD(std::unique_ptr<BlobReader> reader) : m_reader(std::move(re

// Source: http://wiibrew.org/wiki/WAD_files
m_hdr_size = m_reader->ReadSwapped<u32>(0x00).value_or(0);
m_cert_size = m_reader->ReadSwapped<u32>(0x08).value_or(0);
m_tick_size = m_reader->ReadSwapped<u32>(0x10).value_or(0);
m_cert_chain_size = m_reader->ReadSwapped<u32>(0x08).value_or(0);
m_ticket_size = m_reader->ReadSwapped<u32>(0x10).value_or(0);
m_tmd_size = m_reader->ReadSwapped<u32>(0x14).value_or(0);
m_data_size = m_reader->ReadSwapped<u32>(0x18).value_or(0);

m_offset = Common::AlignUp(m_hdr_size, 0x40) + Common::AlignUp(m_cert_size, 0x40);
m_tmd_offset = Common::AlignUp(m_hdr_size, 0x40) + Common::AlignUp(m_cert_size, 0x40) +
Common::AlignUp(m_tick_size, 0x40);
m_opening_bnr_offset =
m_tmd_offset + Common::AlignUp(m_tmd_size, 0x40) + Common::AlignUp(m_data_size, 0x40);
m_cert_chain_offset = Common::AlignUp(m_hdr_size, 0x40);
m_ticket_offset = m_cert_chain_offset + Common::AlignUp(m_cert_chain_size, 0x40);
m_tmd_offset = m_ticket_offset + Common::AlignUp(m_ticket_size, 0x40);
m_data_offset = m_tmd_offset + Common::AlignUp(m_tmd_size, 0x40);
m_opening_bnr_offset = m_data_offset + Common::AlignUp(m_data_size, 0x40);

std::vector<u8> ticket_buffer(m_ticket_size);
Read(m_ticket_offset, m_ticket_size, ticket_buffer.data());
m_ticket.SetBytes(std::move(ticket_buffer));

if (!IOS::ES::IsValidTMDSize(m_tmd_size))
{
@@ -52,6 +56,9 @@ VolumeWAD::VolumeWAD(std::unique_ptr<BlobReader> reader) : m_reader(std::move(re
std::vector<u8> tmd_buffer(m_tmd_size);
Read(m_tmd_offset, m_tmd_size, tmd_buffer.data());
m_tmd.SetBytes(std::move(tmd_buffer));

m_cert_chain.resize(m_cert_chain_size);
Read(m_cert_chain_offset, m_cert_chain_size, m_cert_chain.data());
}

VolumeWAD::~VolumeWAD()
@@ -89,17 +96,43 @@ Country VolumeWAD::GetCountry(const Partition& partition) const
return TypicalCountryForRegion(GetSysMenuRegion(m_tmd.GetTitleVersion()));

const Region region = GetRegion();
if (CountryCodeToRegion(country_byte, Platform::WiiWAD, region) != region)
const std::optional<u16> revision = GetRevision();
if (CountryCodeToRegion(country_byte, Platform::WiiWAD, region, revision) != region)
return TypicalCountryForRegion(region);

return CountryCodeToCountry(country_byte, Platform::WiiWAD, region);
return CountryCodeToCountry(country_byte, Platform::WiiWAD, region, revision);
}

const IOS::ES::TicketReader& VolumeWAD::GetTicket(const Partition& partition) const
{
return m_ticket;
}

const IOS::ES::TMDReader& VolumeWAD::GetTMD(const Partition& partition) const
{
return m_tmd;
}

const std::vector<u8>& VolumeWAD::GetCertificateChain(const Partition& partition) const
{
return m_cert_chain;
}

std::vector<u64> VolumeWAD::GetContentOffsets() const
{
const std::vector<IOS::ES::Content> contents = m_tmd.GetContents();
std::vector<u64> content_offsets;
content_offsets.reserve(contents.size());
u64 offset = m_data_offset;
for (const IOS::ES::Content& content : contents)
{
content_offsets.emplace_back(offset);
offset += Common::AlignUp(content.size, 0x40);
}

return content_offsets;
}

std::string VolumeWAD::GetGameID(const Partition& partition) const
{
return m_tmd.GetGameID();
@@ -126,7 +159,7 @@ std::string VolumeWAD::GetMakerID(const Partition& partition) const

std::optional<u64> VolumeWAD::GetTitleID(const Partition& partition) const
{
return ReadSwapped<u64>(m_offset + 0x01DC, partition);
return ReadSwapped<u64>(m_ticket_offset + 0x01DC, partition);
}

std::optional<u16> VolumeWAD::GetRevision(const Partition& partition) const
@@ -175,6 +208,11 @@ u64 VolumeWAD::GetSize() const
return m_reader->GetDataSize();
}

bool VolumeWAD::IsSizeAccurate() const
{
return m_reader->IsDataSizeAccurate();
}

u64 VolumeWAD::GetRawSize() const
{
return m_reader->GetRawSize();
@@ -33,7 +33,12 @@ class VolumeWAD : public Volume
const Partition& partition = PARTITION_NONE) const override;
const FileSystem* GetFileSystem(const Partition& partition = PARTITION_NONE) const override;
std::optional<u64> GetTitleID(const Partition& partition = PARTITION_NONE) const override;
const IOS::ES::TicketReader&
GetTicket(const Partition& partition = PARTITION_NONE) const override;
const IOS::ES::TMDReader& GetTMD(const Partition& partition = PARTITION_NONE) const override;
const std::vector<u8>&
GetCertificateChain(const Partition& partition = PARTITION_NONE) const override;
std::vector<u64> GetContentOffsets() const override;
std::string GetGameID(const Partition& partition = PARTITION_NONE) const override;
std::string GetGameTDBID(const Partition& partition = PARTITION_NONE) const override;
std::string GetMakerID(const Partition& partition = PARTITION_NONE) const override;
@@ -54,17 +59,22 @@ class VolumeWAD : public Volume

BlobType GetBlobType() const override;
u64 GetSize() const override;
bool IsSizeAccurate() const override;
u64 GetRawSize() const override;

private:
std::unique_ptr<BlobReader> m_reader;
IOS::ES::TicketReader m_ticket;
IOS::ES::TMDReader m_tmd;
u32 m_offset = 0;
std::vector<u8> m_cert_chain;
u32 m_cert_chain_offset = 0;
u32 m_ticket_offset = 0;
u32 m_tmd_offset = 0;
u32 m_data_offset = 0;
u32 m_opening_bnr_offset = 0;
u32 m_hdr_size = 0;
u32 m_cert_size = 0;
u32 m_tick_size = 0;
u32 m_cert_chain_size = 0;
u32 m_ticket_size = 0;
u32 m_tmd_size = 0;
u32 m_data_size = 0;
};
@@ -97,6 +97,31 @@ VolumeWii::VolumeWii(std::unique_ptr<BlobReader> reader)
return IOS::ES::TMDReader{std::move(tmd_buffer)};
};

auto get_cert_chain = [this, partition]() -> std::vector<u8> {
const std::optional<u32> size = m_reader->ReadSwapped<u32>(partition.offset + 0x2ac);
const std::optional<u64> address =
ReadSwappedAndShifted(partition.offset + 0x2b0, PARTITION_NONE);
if (!size || !address)
return {};
std::vector<u8> cert_chain(*size);
if (!m_reader->Read(partition.offset + *address, *size, cert_chain.data()))
return {};
return cert_chain;
};

auto get_h3_table = [this, partition]() -> std::vector<u8> {
if (!m_encrypted)
return {};
const std::optional<u64> h3_table_offset =
ReadSwappedAndShifted(partition.offset + 0x2b4, PARTITION_NONE);
if (!h3_table_offset)
return {};
std::vector<u8> h3_table(H3_TABLE_SIZE);
if (!m_reader->Read(partition.offset + *h3_table_offset, H3_TABLE_SIZE, h3_table.data()))
return {};
return h3_table;
};

auto get_key = [this, partition]() -> std::unique_ptr<mbedtls_aes_context> {
const IOS::ES::TicketReader& ticket = *m_partitions[partition].ticket;
if (!ticket.IsValid())
@@ -120,6 +145,8 @@ VolumeWii::VolumeWii(std::unique_ptr<BlobReader> reader)
partition, PartitionDetails{Common::Lazy<std::unique_ptr<mbedtls_aes_context>>(get_key),
Common::Lazy<IOS::ES::TicketReader>(get_ticket),
Common::Lazy<IOS::ES::TMDReader>(get_tmd),
Common::Lazy<std::vector<u8>>(get_cert_chain),
Common::Lazy<std::vector<u8>>(get_h3_table),
Common::Lazy<std::unique_ptr<FileSystem>>(get_file_system),
Common::Lazy<u64>(get_data_offset), *partition_type});
}
@@ -239,6 +266,12 @@ const IOS::ES::TMDReader& VolumeWii::GetTMD(const Partition& partition) const
return it != m_partitions.end() ? *it->second.tmd : INVALID_TMD;
}

const std::vector<u8>& VolumeWii::GetCertificateChain(const Partition& partition) const
{
auto it = m_partitions.find(partition);
return it != m_partitions.end() ? *it->second.cert_chain : INVALID_CERT_CHAIN;
}

const FileSystem* VolumeWii::GetFileSystem(const Partition& partition) const
{
auto it = m_partitions.find(partition);
@@ -297,11 +330,12 @@ Country VolumeWii::GetCountry(const Partition& partition) const
// The 0 that we use as a default value is mapped to Country::Unknown and Region::Unknown
const u8 country_byte = ReadSwapped<u8>(3, partition).value_or(0);
const Region region = GetRegion();
const std::optional<u16> revision = GetRevision();

if (CountryCodeToRegion(country_byte, Platform::WiiDisc, region) != region)
if (CountryCodeToRegion(country_byte, Platform::WiiDisc, region, revision) != region)
return TypicalCountryForRegion(region);

return CountryCodeToCountry(country_byte, Platform::WiiDisc, region);
return CountryCodeToCountry(country_byte, Platform::WiiDisc, region, revision);
}

std::string VolumeWii::GetMakerID(const Partition& partition) const
@@ -379,88 +413,95 @@ u64 VolumeWii::GetSize() const
return m_reader->GetDataSize();
}

bool VolumeWii::IsSizeAccurate() const
{
return m_reader->IsDataSizeAccurate();
}

u64 VolumeWii::GetRawSize() const
{
return m_reader->GetRawSize();
}

bool VolumeWii::CheckIntegrity(const Partition& partition) const
bool VolumeWii::CheckH3TableIntegrity(const Partition& partition) const
{
if (!m_encrypted)
auto it = m_partitions.find(partition);
if (it == m_partitions.end())
return false;
const PartitionDetails& partition_details = it->second;

const std::vector<u8>& h3_table = *partition_details.h3_table;
if (h3_table.size() != H3_TABLE_SIZE)
return false;

// Get the decryption key for the partition
const IOS::ES::TMDReader& tmd = *partition_details.tmd;
if (!tmd.IsValid())
return false;

const std::vector<IOS::ES::Content> contents = tmd.GetContents();
if (contents.size() != 1)
return false;

std::array<u8, 20> h3_table_sha1;
mbedtls_sha1(h3_table.data(), h3_table.size(), h3_table_sha1.data());
return h3_table_sha1 == contents[0].sha1;
}

bool VolumeWii::CheckBlockIntegrity(u64 block_index, const Partition& partition) const
{
auto it = m_partitions.find(partition);
if (it == m_partitions.end())
return false;
const PartitionDetails& partition_details = it->second;

constexpr size_t SHA1_SIZE = 20;
if (block_index / 64 * SHA1_SIZE >= partition_details.h3_table->size())
return false;

mbedtls_aes_context* aes_context = partition_details.key->get();
if (!aes_context)
return false;

// Get partition data size
const auto part_data_size = ReadSwappedAndShifted(partition.offset + 0x2BC, PARTITION_NONE);
if (!part_data_size)
const u64 cluster_offset =
partition.offset + *partition_details.data_offset + block_index * BLOCK_TOTAL_SIZE;

// Read and decrypt the cluster metadata
u8 cluster_metadata_crypted[BLOCK_HEADER_SIZE];
u8 cluster_metadata[BLOCK_HEADER_SIZE];
u8 iv[16] = {0};
if (!m_reader->Read(cluster_offset, BLOCK_HEADER_SIZE, cluster_metadata_crypted))
return false;
mbedtls_aes_crypt_cbc(aes_context, MBEDTLS_AES_DECRYPT, BLOCK_HEADER_SIZE, iv,
cluster_metadata_crypted, cluster_metadata);

const u32 num_clusters = static_cast<u32>(part_data_size.value() / 0x8000);
for (u32 cluster_id = 0; cluster_id < num_clusters; ++cluster_id)
{
const u64 cluster_offset =
partition.offset + *partition_details.data_offset + static_cast<u64>(cluster_id) * 0x8000;

// Read and decrypt the cluster metadata
u8 cluster_metadata_crypted[0x400];
u8 cluster_metadata[0x400];
u8 iv[16] = {0};
if (!m_reader->Read(cluster_offset, sizeof(cluster_metadata_crypted), cluster_metadata_crypted))
{
WARN_LOG(DISCIO, "Integrity Check: fail at cluster %d: could not read metadata", cluster_id);
return false;
}
mbedtls_aes_crypt_cbc(aes_context, MBEDTLS_AES_DECRYPT, sizeof(cluster_metadata), iv,
cluster_metadata_crypted, cluster_metadata);

// Some clusters have invalid data and metadata because they aren't
// meant to be read by the game (for example, holes between files). To
// try to avoid reporting errors because of these clusters, we check
// the 0x00 paddings in the metadata.
//
// This may cause some false negatives though: some bad clusters may be
// skipped because they are *too* bad and are not even recognized as
// valid clusters. To be improved.
const u8* pad_begin = cluster_metadata + 0x26C;
const u8* pad_end = pad_begin + 0x14;
const bool meaningless = std::any_of(pad_begin, pad_end, [](u8 val) { return val != 0; });

if (meaningless)
continue;
u8 cluster_data[BLOCK_DATA_SIZE];
if (!Read(block_index * BLOCK_DATA_SIZE, BLOCK_DATA_SIZE, cluster_data, partition))
return false;

u8 cluster_data[0x7C00];
if (!Read(cluster_id * sizeof(cluster_data), sizeof(cluster_data), cluster_data, partition))
{
WARN_LOG(DISCIO, "Integrity Check: fail at cluster %d: could not read data", cluster_id);
for (u32 hash_index = 0; hash_index < 31; ++hash_index)
{
u8 h0_hash[SHA1_SIZE];
mbedtls_sha1(cluster_data + hash_index * 0x400, 0x400, h0_hash);
if (memcmp(h0_hash, cluster_metadata + hash_index * SHA1_SIZE, SHA1_SIZE))
return false;
}
}

for (u32 hash_id = 0; hash_id < 31; ++hash_id)
{
u8 hash[20];
u8 h1_hash[SHA1_SIZE];
mbedtls_sha1(cluster_metadata, SHA1_SIZE * 31, h1_hash);
if (memcmp(h1_hash, cluster_metadata + 0x280 + (block_index % 8) * SHA1_SIZE, SHA1_SIZE))
return false;

mbedtls_sha1(cluster_data + hash_id * sizeof(cluster_metadata), sizeof(cluster_metadata),
hash);
u8 h2_hash[SHA1_SIZE];
mbedtls_sha1(cluster_metadata + 0x280, SHA1_SIZE * 8, h2_hash);
if (memcmp(h2_hash, cluster_metadata + 0x340 + (block_index / 8 % 8) * SHA1_SIZE, SHA1_SIZE))
return false;

// Note that we do not use strncmp here
if (memcmp(hash, cluster_metadata + hash_id * sizeof(hash), sizeof(hash)))
{
WARN_LOG(DISCIO, "Integrity Check: fail at cluster %d: hash %d is invalid", cluster_id,
hash_id);
return false;
}
}
}
u8 h3_hash[SHA1_SIZE];
mbedtls_sha1(cluster_metadata + 0x340, SHA1_SIZE * 8, h3_hash);
if (memcmp(h3_hash, partition_details.h3_table->data() + block_index / 64 * SHA1_SIZE, SHA1_SIZE))
return false;

return true;
}

} // namespace
} // namespace DiscIO
@@ -40,30 +40,35 @@ class VolumeWii : public Volume
std::optional<u64> GetTitleID(const Partition& partition) const override;
const IOS::ES::TicketReader& GetTicket(const Partition& partition) const override;
const IOS::ES::TMDReader& GetTMD(const Partition& partition) const override;
const std::vector<u8>& GetCertificateChain(const Partition& partition) const override;
const FileSystem* GetFileSystem(const Partition& partition) const override;
static u64 EncryptedPartitionOffsetToRawOffset(u64 offset, const Partition& partition,
u64 partition_data_offset);
u64 PartitionOffsetToRawOffset(u64 offset, const Partition& partition) const override;
std::string GetGameID(const Partition& partition) const override;
std::string GetGameTDBID(const Partition& partition) const override;
std::string GetMakerID(const Partition& partition) const override;
std::optional<u16> GetRevision(const Partition& partition) const override;
std::string GetInternalName(const Partition& partition) const override;
std::string GetGameID(const Partition& partition = PARTITION_NONE) const override;
std::string GetGameTDBID(const Partition& partition = PARTITION_NONE) const override;
std::string GetMakerID(const Partition& partition = PARTITION_NONE) const override;
std::optional<u16> GetRevision(const Partition& partition = PARTITION_NONE) const override;
std::string GetInternalName(const Partition& partition = PARTITION_NONE) const override;
std::map<Language, std::string> GetLongNames() const override;
std::vector<u32> GetBanner(u32* width, u32* height) const override;
std::string GetApploaderDate(const Partition& partition) const override;
std::optional<u8> GetDiscNumber(const Partition& partition) const override;
std::optional<u8> GetDiscNumber(const Partition& partition = PARTITION_NONE) const override;

Platform GetVolumeType() const override;
bool SupportsIntegrityCheck() const override { return true; }
bool CheckIntegrity(const Partition& partition) const override;
bool SupportsIntegrityCheck() const override { return m_encrypted; }
bool CheckH3TableIntegrity(const Partition& partition) const override;
bool CheckBlockIntegrity(u64 block_index, const Partition& partition) const override;

Region GetRegion() const override;
Country GetCountry(const Partition& partition) const override;
Country GetCountry(const Partition& partition = PARTITION_NONE) const override;
BlobType GetBlobType() const override;
u64 GetSize() const override;
bool IsSizeAccurate() const override;
u64 GetRawSize() const override;

static constexpr unsigned int H3_TABLE_SIZE = 0x18000;

static constexpr unsigned int BLOCK_HEADER_SIZE = 0x0400;
static constexpr unsigned int BLOCK_DATA_SIZE = 0x7C00;
static constexpr unsigned int BLOCK_TOTAL_SIZE = BLOCK_HEADER_SIZE + BLOCK_DATA_SIZE;
@@ -77,6 +82,8 @@ class VolumeWii : public Volume
Common::Lazy<std::unique_ptr<mbedtls_aes_context>> key;
Common::Lazy<IOS::ES::TicketReader> ticket;
Common::Lazy<IOS::ES::TMDReader> tmd;
Common::Lazy<std::vector<u8>> cert_chain;
Common::Lazy<std::vector<u8>> h3_table;
Common::Lazy<std::unique_ptr<FileSystem>> file_system;
Common::Lazy<u64> data_offset;
u32 type;
@@ -24,12 +24,13 @@ class WbfsFileReader : public BlobReader
static std::unique_ptr<WbfsFileReader> Create(File::IOFile file, const std::string& path);

BlobType GetBlobType() const override { return BlobType::WBFS; }
u64 GetRawSize() const override { return m_size; }
// The WBFS format does not save the original file size.
// This function returns a constant upper bound
// (the size of a double-layer Wii disc).
u64 GetDataSize() const override;
bool IsDataSizeAccurate() const override { return false; }

u64 GetRawSize() const override { return m_size; }
bool Read(u64 offset, u64 nbytes, u8* out_ptr) override;

private:
@@ -76,6 +76,7 @@ add_executable(dolphin-emu
Config/PatchesWidget.cpp
Config/PropertiesDialog.cpp
Config/SettingsWindow.cpp
Config/VerifyWidget.cpp
Debugger/BreakpointWidget.cpp
Debugger/CodeViewWidget.cpp
Debugger/CodeWidget.cpp
@@ -40,8 +40,8 @@ enum class EntryType
};
Q_DECLARE_METATYPE(EntryType);

FilesystemWidget::FilesystemWidget(const UICommon::GameFile& game)
: m_game(game), m_volume(DiscIO::CreateVolumeFromFilename(game.GetFilePath()))
FilesystemWidget::FilesystemWidget(std::shared_ptr<DiscIO::Volume> volume)
: m_volume(std::move(volume))
{
CreateWidgets();
ConnectWidgets();
@@ -225,8 +225,7 @@ void FilesystemWidget::ShowContextMenu(const QPoint&)
{
if (const std::optional<u32> partition_type = m_volume->GetPartitionType(p))
{
const std::string partition_name =
DiscIO::DirectoryNameForPartitionType(*partition_type);
const std::string partition_name = DiscIO::NameForPartitionType(*partition_type, true);
ExtractPartition(p, folder + QChar(u'/') + QString::fromStdString(partition_name));
}
}
@@ -239,12 +238,6 @@ void FilesystemWidget::ShowContextMenu(const QPoint&)
if (!folder.isEmpty())
ExtractPartition(partition, folder);
});
if (m_volume->IsEncryptedAndHashed())
{
menu->addSeparator();
menu->addAction(tr("Check Partition Integrity"), this,
[this, partition] { CheckIntegrity(partition); });
}
break;
case EntryType::File:
menu->addAction(tr("Extract File..."), this, [this, partition, path] {
@@ -328,35 +321,3 @@ void FilesystemWidget::ExtractFile(const DiscIO::Partition& partition, const QSt
else
ModalMessageBox::critical(this, tr("Error"), tr("Failed to extract file."));
}

void FilesystemWidget::CheckIntegrity(const DiscIO::Partition& partition)
{
QProgressDialog* dialog = new QProgressDialog(this);
std::future<bool> is_valid = std::async(
std::launch::async, [this, partition] { return m_volume->CheckIntegrity(partition); });

dialog->setLabelText(tr("Verifying integrity of partition..."));
dialog->setWindowFlags(dialog->windowFlags() & ~Qt::WindowContextHelpButtonHint);
dialog->setWindowTitle(tr("Verifying partition"));

dialog->setMinimum(0);
dialog->setMaximum(0);
dialog->show();

while (is_valid.wait_for(std::chrono::milliseconds(50)) != std::future_status::ready)
QCoreApplication::processEvents();

dialog->close();

if (is_valid.get())
{
ModalMessageBox::information(this, tr("Success"),
tr("Integrity check completed. No errors have been found."));
}
else
{
ModalMessageBox::critical(this, tr("Error"),
tr("Integrity check for partition failed. The disc image is most "
"likely corrupted or has been patched incorrectly."));
}
}
@@ -8,8 +8,6 @@
#include <QIcon>
#include <memory>

#include "UICommon/GameFile.h"

class QStandardItem;
class QStandardItemModel;
class QTreeView;
@@ -26,7 +24,7 @@ class FilesystemWidget final : public QWidget
{
Q_OBJECT
public:
explicit FilesystemWidget(const UICommon::GameFile& game);
explicit FilesystemWidget(std::shared_ptr<DiscIO::Volume> volume);
~FilesystemWidget() override;

private:
@@ -45,15 +43,13 @@ class FilesystemWidget final : public QWidget
const QString& out);
void ExtractFile(const DiscIO::Partition& partition, const QString& path, const QString& out);
bool ExtractSystemData(const DiscIO::Partition& partition, const QString& out);
void CheckIntegrity(const DiscIO::Partition& partition);

DiscIO::Partition GetPartitionFromID(int id);

QStandardItemModel* m_tree_model;
QTreeView* m_tree_view;

UICommon::GameFile m_game;
std::unique_ptr<DiscIO::Volume> m_volume;
std::shared_ptr<DiscIO::Volume> m_volume;

QIcon m_folder_icon;
QIcon m_file_icon;
@@ -78,7 +78,6 @@ QGroupBox* InfoWidget::CreateISODetails()
QLineEdit* maker =
CreateValueDisplay((game_maker.empty() ? UNKNOWN_NAME.toStdString() : game_maker) + " (" +
m_game.GetMakerID() + ")");
QWidget* checksum = CreateChecksumComputer();

layout->addRow(tr("Name:"), internal_name);
layout->addRow(tr("File:"), file_path);
@@ -89,8 +88,6 @@ QGroupBox* InfoWidget::CreateISODetails()
if (!m_game.GetApploaderDate().empty())
layout->addRow(tr("Apploader Date:"), CreateValueDisplay(m_game.GetApploaderDate()));

layout->addRow(tr("MD5 Checksum:"), checksum);

group->setLayout(layout);
return group;
}
@@ -198,53 +195,3 @@ void InfoWidget::ChangeLanguage()
if (m_description)
m_description->setText(QString::fromStdString(m_game.GetDescription(language)));
}

QWidget* InfoWidget::CreateChecksumComputer()
{
QWidget* widget = new QWidget();
QHBoxLayout* layout = new QHBoxLayout();
layout->setContentsMargins(0, 0, 0, 0);

m_checksum_result = new QLineEdit();
m_checksum_result->setReadOnly(true);
QPushButton* calculate = new QPushButton(tr("Compute"));
connect(calculate, &QPushButton::clicked, this, &InfoWidget::ComputeChecksum);
layout->addWidget(m_checksum_result);
layout->addWidget(calculate);

widget->setLayout(layout);
return widget;
}

void InfoWidget::ComputeChecksum()
{
QCryptographicHash hash(QCryptographicHash::Md5);
hash.reset();
std::unique_ptr<DiscIO::BlobReader> file(DiscIO::CreateBlobReader(m_game.GetFilePath()));
std::vector<u8> file_data(8 * 1080 * 1080); // read 1MB at a time
u64 game_size = file->GetDataSize();
u64 read_offset = 0;

// a maximum of 1000 is used instead of game_size because otherwise 8GB games overflow the int
// typed maximum parameter
QProgressDialog* progress =
new QProgressDialog(tr("Computing MD5 Checksum"), tr("Cancel"), 0, 1000, this);
progress->setWindowTitle(tr("Computing MD5 Checksum"));
progress->setWindowFlags(progress->windowFlags() & ~Qt::WindowContextHelpButtonHint);
progress->setMinimumDuration(500);
progress->setWindowModality(Qt::WindowModal);
while (read_offset < game_size)
{
progress->setValue(static_cast<double>(read_offset) / static_cast<double>(game_size) * 1000);
if (progress->wasCanceled())
return;

u64 read_size = std::min<u64>(file_data.size(), game_size - read_offset);
file->Read(read_offset, read_size, file_data.data());
hash.addData(reinterpret_cast<char*>(file_data.data()), read_size);
read_offset += read_size;
}
m_checksum_result->setText(QString::fromUtf8(hash.result().toHex()));
Q_ASSERT(read_offset == game_size);
progress->setValue(1000);
}
@@ -23,20 +23,17 @@ class InfoWidget final : public QWidget
explicit InfoWidget(const UICommon::GameFile& game);

private:
void ComputeChecksum();
void ChangeLanguage();
void SaveBanner();

QGroupBox* CreateBannerDetails();
QGroupBox* CreateISODetails();
QLineEdit* CreateValueDisplay(const QString& value);
QLineEdit* CreateValueDisplay(const std::string& value = "");
QWidget* CreateChecksumComputer();
void CreateLanguageSelector();
QWidget* CreateBannerGraphic(const QPixmap& image);

UICommon::GameFile m_game;
QLineEdit* m_checksum_result;
QComboBox* m_language_selector;
QLineEdit* m_name = {};
QLineEdit* m_maker = {};
@@ -2,12 +2,15 @@
// Licensed under GPLv2+
// Refer to the license.txt file included.

#include <memory>

#include <QDialogButtonBox>
#include <QPushButton>
#include <QTabWidget>
#include <QVBoxLayout>

#include "DiscIO/Enums.h"
#include "DiscIO/Volume.h"

#include "DolphinQt/Config/ARCodeWidget.h"
#include "DolphinQt/Config/FilesystemWidget.h"
@@ -16,6 +19,7 @@
#include "DolphinQt/Config/InfoWidget.h"
#include "DolphinQt/Config/PatchesWidget.h"
#include "DolphinQt/Config/PropertiesDialog.h"
#include "DolphinQt/Config/VerifyWidget.h"
#include "DolphinQt/QtUtils/WrapInScrollArea.h"

#include "UICommon/GameFile.h"
@@ -54,11 +58,22 @@ PropertiesDialog::PropertiesDialog(QWidget* parent, const UICommon::GameFile& ga
tr("Gecko Codes"));
tab_widget->addTab(GetWrappedWidget(info, this, padding_width, padding_height), tr("Info"));

if (DiscIO::IsDisc(game.GetPlatform()))
if (game.GetPlatform() != DiscIO::Platform::ELFOrDOL)
{
FilesystemWidget* filesystem = new FilesystemWidget(game);
tab_widget->addTab(GetWrappedWidget(filesystem, this, padding_width, padding_height),
tr("Filesystem"));
std::shared_ptr<DiscIO::Volume> volume = DiscIO::CreateVolumeFromFilename(game.GetFilePath());
if (volume)
{
VerifyWidget* verify = new VerifyWidget(volume);
tab_widget->addTab(GetWrappedWidget(verify, this, padding_width, padding_height),
tr("Verify"));

if (DiscIO::IsDisc(game.GetPlatform()))
{
FilesystemWidget* filesystem = new FilesystemWidget(volume);
tab_widget->addTab(GetWrappedWidget(filesystem, this, padding_width, padding_height),
tr("Filesystem"));
}
}
}

layout->addWidget(tab_widget);
@@ -0,0 +1,156 @@
// Copyright 2019 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.

#include "DolphinQt/Config/VerifyWidget.h"

#include <memory>
#include <tuple>
#include <vector>

#include <QByteArray>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QLabel>
#include <QProgressDialog>
#include <QVBoxLayout>

#include "Common/CommonTypes.h"
#include "DiscIO/Volume.h"
#include "DiscIO/VolumeVerifier.h"

VerifyWidget::VerifyWidget(std::shared_ptr<DiscIO::Volume> volume) : m_volume(std::move(volume))
{
QVBoxLayout* layout = new QVBoxLayout(this);

CreateWidgets();
ConnectWidgets();

layout->addWidget(m_problems);
layout->addWidget(m_summary_text);
layout->addLayout(m_hash_layout);
layout->addWidget(m_verify_button);

layout->setStretchFactor(m_problems, 5);
layout->setStretchFactor(m_summary_text, 2);

setLayout(layout);
}

void VerifyWidget::CreateWidgets()
{
m_problems = new QTableWidget(0, 2, this);
m_problems->setHorizontalHeaderLabels({tr("Problem"), tr("Severity")});
m_problems->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch);
m_problems->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
m_problems->horizontalHeader()->setHighlightSections(false);
m_problems->verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
m_problems->verticalHeader()->hide();

m_summary_text = new QTextEdit(this);
m_summary_text->setReadOnly(true);

m_hash_layout = new QFormLayout(this);
std::tie(m_crc32_checkbox, m_crc32_line_edit) = AddHashLine(m_hash_layout, tr("CRC32:"));
std::tie(m_md5_checkbox, m_md5_line_edit) = AddHashLine(m_hash_layout, tr("MD5:"));
std::tie(m_sha1_checkbox, m_sha1_line_edit) = AddHashLine(m_hash_layout, tr("SHA-1:"));

m_verify_button = new QPushButton(tr("Verify Integrity"), this);
}

std::pair<QCheckBox*, QLineEdit*> VerifyWidget::AddHashLine(QFormLayout* layout, QString text)
{
QLineEdit* line_edit = new QLineEdit(this);
line_edit->setReadOnly(true);
QCheckBox* checkbox = new QCheckBox(tr("Calculate"), this);
checkbox->setChecked(true);

QHBoxLayout* hbox_layout = new QHBoxLayout(this);
hbox_layout->addWidget(line_edit);
hbox_layout->addWidget(checkbox);

layout->addRow(text, hbox_layout);

return std::pair(checkbox, line_edit);
}

void VerifyWidget::ConnectWidgets()
{
connect(m_verify_button, &QPushButton::clicked, this, &VerifyWidget::Verify);
}

static void SetHash(QLineEdit* line_edit, const std::vector<u8>& hash)
{
const QByteArray byte_array = QByteArray::fromRawData(reinterpret_cast<const char*>(hash.data()),
static_cast<int>(hash.size()));
line_edit->setText(QString::fromLatin1(byte_array.toHex()));
}

void VerifyWidget::Verify()
{
DiscIO::VolumeVerifier verifier(
*m_volume,
{m_crc32_checkbox->isChecked(), m_md5_checkbox->isChecked(), m_sha1_checkbox->isChecked()});

// We have to divide the number of processed bytes with something so it won't make ints overflow
constexpr int DIVISOR = 0x100;

QProgressDialog* progress = new QProgressDialog(tr("Verifying"), tr("Cancel"), 0,
verifier.GetTotalBytes() / DIVISOR, this);
progress->setWindowTitle(tr("Verifying"));
progress->setWindowFlags(progress->windowFlags() & ~Qt::WindowContextHelpButtonHint);
progress->setMinimumDuration(500);
progress->setWindowModality(Qt::WindowModal);

verifier.Start();
while (verifier.GetBytesProcessed() != verifier.GetTotalBytes())
{
progress->setValue(verifier.GetBytesProcessed() / DIVISOR);
if (progress->wasCanceled())
return;

verifier.Process();
}
verifier.Finish();

DiscIO::VolumeVerifier::Result result = verifier.GetResult();
progress->setValue(verifier.GetBytesProcessed() / DIVISOR);

m_summary_text->setText(QString::fromStdString(result.summary_text));

m_problems->setRowCount(static_cast<int>(result.problems.size()));
for (int i = 0; i < m_problems->rowCount(); ++i)
{
const DiscIO::VolumeVerifier::Problem problem = result.problems[i];

QString severity;
switch (problem.severity)
{
case DiscIO::VolumeVerifier::Severity::Low:
severity = tr("Low");
break;
case DiscIO::VolumeVerifier::Severity::Medium:
severity = tr("Medium");
break;
case DiscIO::VolumeVerifier::Severity::High:
severity = tr("High");
break;
}

SetProblemCellText(i, 0, QString::fromStdString(problem.text));
SetProblemCellText(i, 1, severity);
}

SetHash(m_crc32_line_edit, result.hashes.crc32);
SetHash(m_md5_line_edit, result.hashes.md5);
SetHash(m_sha1_line_edit, result.hashes.sha1);
}

void VerifyWidget::SetProblemCellText(int row, int column, QString text)
{
QLabel* label = new QLabel(text);
label->setTextInteractionFlags(Qt::TextSelectableByMouse);
label->setWordWrap(true);
label->setMargin(4);
m_problems->setCellWidget(row, column, label);
}
@@ -0,0 +1,49 @@
// Copyright 2019 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.

#pragma once

#include <memory>
#include <string>
#include <utility>

#include <QCheckBox>
#include <QFormLayout>
#include <QLineEdit>
#include <QPushButton>
#include <QTableWidget>
#include <QTextEdit>
#include <QWidget>

namespace DiscIO
{
class Volume;
}

class VerifyWidget final : public QWidget
{
Q_OBJECT
public:
explicit VerifyWidget(std::shared_ptr<DiscIO::Volume> volume);

private:
void CreateWidgets();
std::pair<QCheckBox*, QLineEdit*> AddHashLine(QFormLayout* layout, QString text);
void ConnectWidgets();

void Verify();
void SetProblemCellText(int row, int column, QString text);

std::shared_ptr<DiscIO::Volume> m_volume;
QTableWidget* m_problems;
QTextEdit* m_summary_text;
QFormLayout* m_hash_layout;
QCheckBox* m_crc32_checkbox;
QCheckBox* m_md5_checkbox;
QCheckBox* m_sha1_checkbox;
QLineEdit* m_crc32_line_edit;
QLineEdit* m_md5_line_edit;
QLineEdit* m_sha1_line_edit;
QPushButton* m_verify_button;
};
@@ -108,6 +108,7 @@
<QtMoc Include="Config\PatchesWidget.h" />
<QtMoc Include="Config\PropertiesDialog.h" />
<QtMoc Include="Config\SettingsWindow.h" />
<QtMoc Include="Config\VerifyWidget.h" />
<QtMoc Include="DiscordHandler.h" />
<QtMoc Include="DiscordJoinRequestDialog.h" />
<QtMoc Include="FIFO\FIFOAnalyzer.h" />
@@ -269,6 +270,7 @@
<ClCompile Include="$(QtMocOutPrefix)ToolBar.cpp" />
<ClCompile Include="$(QtMocOutPrefix)USBDeviceAddToWhitelistDialog.cpp" />
<ClCompile Include="$(QtMocOutPrefix)Updater.cpp" />
<ClCompile Include="$(QtMocOutPrefix)VerifyWidget.cpp" />
<ClCompile Include="$(QtMocOutPrefix)WatchWidget.cpp" />
<ClCompile Include="$(QtMocOutPrefix)WiiPane.cpp" />
<ClCompile Include="$(QtMocOutPrefix)WiiTASInputWindow.cpp" />
@@ -329,6 +331,7 @@
<ClCompile Include="Config\PatchesWidget.cpp" />
<ClCompile Include="Config\PropertiesDialog.cpp" />
<ClCompile Include="Config\SettingsWindow.cpp" />
<ClCompile Include="Config\VerifyWidget.cpp" />
<ClCompile Include="Debugger\CodeViewWidget.cpp" />
<ClCompile Include="Debugger\CodeWidget.cpp" />
<ClCompile Include="Debugger\JITWidget.cpp" />