306 changes: 306 additions & 0 deletions Source/Core/DiscIO/NFSBlob.cpp
@@ -0,0 +1,306 @@
// Copyright 2022 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#include "DiscIO/NFSBlob.h"

#include <algorithm>
#include <array>
#include <cstring>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include <fmt/format.h>

#include "Common/Align.h"
#include "Common/CommonTypes.h"
#include "Common/Crypto/AES.h"
#include "Common/IOFile.h"
#include "Common/Logging/Log.h"
#include "Common/StringUtil.h"
#include "Common/Swap.h"

namespace DiscIO
{
bool NFSFileReader::ReadKey(const std::string& path, const std::string& directory, Key* key_out)
{
const std::string_view directory_without_trailing_slash =
std::string_view(directory).substr(0, directory.size() - 1);

std::string parent, parent_name, parent_extension;
SplitPath(directory_without_trailing_slash, &parent, &parent_name, &parent_extension);

if (parent_name + parent_extension != "content")
{
ERROR_LOG_FMT(DISCIO, "hif_000000.nfs is not in a directory named 'content': {}", path);
return false;
}

const std::string key_path = parent + "code/htk.bin";
File::IOFile key_file(key_path, "rb");
if (!key_file.ReadBytes(key_out->data(), key_out->size()))
{
ERROR_LOG_FMT(DISCIO, "Failed to read from {}", key_path);
return false;
}

return true;
}

std::vector<NFSLBARange> NFSFileReader::GetLBARanges(const NFSHeader& header)
{
const size_t lba_range_count =
std::min<size_t>(Common::swap32(header.lba_range_count), header.lba_ranges.size());

std::vector<NFSLBARange> lba_ranges;
lba_ranges.reserve(lba_range_count);

for (size_t i = 0; i < lba_range_count; ++i)
{
const NFSLBARange& unswapped_lba_range = header.lba_ranges[i];
lba_ranges.push_back(NFSLBARange{Common::swap32(unswapped_lba_range.start_block),
Common::swap32(unswapped_lba_range.num_blocks)});
}

return lba_ranges;
}

std::vector<File::IOFile> NFSFileReader::OpenFiles(const std::string& directory,
File::IOFile first_file, u64 expected_raw_size,
u64* raw_size_out)
{
const u64 file_count = Common::AlignUp(expected_raw_size, MAX_FILE_SIZE) / MAX_FILE_SIZE;

std::vector<File::IOFile> files;
files.reserve(file_count);

u64 raw_size = first_file.GetSize();
files.emplace_back(std::move(first_file));

for (u64 i = 1; i < file_count; ++i)
{
const std::string child_path = fmt::format("{}hif_{:06}.nfs", directory, i);
File::IOFile child(child_path, "rb");
if (!child)
{
ERROR_LOG_FMT(DISCIO, "Failed to open {}", child_path);
return {};
}

raw_size += child.GetSize();
files.emplace_back(std::move(child));
}

if (raw_size < expected_raw_size)
{
ERROR_LOG_FMT(
DISCIO,
"Expected sum of NFS file sizes for {} to be at least {} bytes, but it was {} bytes",
directory, expected_raw_size, raw_size);
return {};
}

return files;
}

u64 NFSFileReader::CalculateExpectedRawSize(const std::vector<NFSLBARange>& lba_ranges)
{
u64 total_blocks = 0;
for (const NFSLBARange& range : lba_ranges)
total_blocks += range.num_blocks;

return sizeof(NFSHeader) + total_blocks * BLOCK_SIZE;
}

u64 NFSFileReader::CalculateExpectedDataSize(const std::vector<NFSLBARange>& lba_ranges)
{
u32 greatest_block_index = 0;
for (const NFSLBARange& range : lba_ranges)
greatest_block_index = std::max(greatest_block_index, range.start_block + range.num_blocks);

return u64(greatest_block_index) * BLOCK_SIZE;
}

std::unique_ptr<NFSFileReader> NFSFileReader::Create(File::IOFile first_file,
const std::string& path)
{
std::string directory, filename, extension;
SplitPath(path, &directory, &filename, &extension);
if (filename + extension != "hif_000000.nfs")
return nullptr;

std::array<u8, 16> key;
if (!ReadKey(path, directory, &key))
return nullptr;

NFSHeader header;
if (!first_file.Seek(0, File::SeekOrigin::Begin) ||
!first_file.ReadArray(&header, 1) && header.magic != NFS_MAGIC)
{
return nullptr;
}

std::vector<NFSLBARange> lba_ranges = GetLBARanges(header);

const u64 expected_raw_size = CalculateExpectedRawSize(lba_ranges);

u64 raw_size;
std::vector<File::IOFile> files =
OpenFiles(directory, std::move(first_file), expected_raw_size, &raw_size);

if (files.empty())
return nullptr;

return std::unique_ptr<NFSFileReader>(
new NFSFileReader(std::move(lba_ranges), std::move(files), key, raw_size));
}

NFSFileReader::NFSFileReader(std::vector<NFSLBARange> lba_ranges, std::vector<File::IOFile> files,
Key key, u64 raw_size)
: m_lba_ranges(std::move(lba_ranges)), m_files(std::move(files)),
m_aes_context(Common::AES::CreateContextDecrypt(key.data())), m_raw_size(raw_size)
{
m_data_size = CalculateExpectedDataSize(m_lba_ranges);
}

u64 NFSFileReader::GetDataSize() const
{
return m_data_size;
}

u64 NFSFileReader::GetRawSize() const
{
return m_raw_size;
}

u64 NFSFileReader::ToPhysicalBlockIndex(u64 logical_block_index)
{
u64 physical_blocks_so_far = 0;

for (const NFSLBARange& range : m_lba_ranges)
{
if (logical_block_index >= range.start_block &&
logical_block_index < range.start_block + range.num_blocks)
{
return physical_blocks_so_far + (logical_block_index - range.start_block);
}

physical_blocks_so_far += range.num_blocks;
}

return std::numeric_limits<u64>::max();
}

bool NFSFileReader::ReadEncryptedBlock(u64 physical_block_index)
{
constexpr u64 BLOCKS_PER_FILE = MAX_FILE_SIZE / BLOCK_SIZE;

const u64 file_index = physical_block_index / BLOCKS_PER_FILE;
const u64 block_in_file = physical_block_index % BLOCKS_PER_FILE;

if (block_in_file == BLOCKS_PER_FILE - 1)
{
// Special case. Because of the 0x200 byte header at the very beginning,
// the last block of each file has its last 0x200 bytes stored in the next file.

constexpr size_t PART_1_SIZE = BLOCK_SIZE - sizeof(NFSHeader);
constexpr size_t PART_2_SIZE = sizeof(NFSHeader);

File::IOFile& file_1 = m_files[file_index];
File::IOFile& file_2 = m_files[file_index + 1];

if (!file_1.Seek(sizeof(NFSHeader) + block_in_file * BLOCK_SIZE, File::SeekOrigin::Begin) ||
!file_1.ReadBytes(m_current_block_encrypted.data(), PART_1_SIZE))
{
file_1.ClearError();
return false;
}

if (!file_2.Seek(0, File::SeekOrigin::Begin) ||
!file_2.ReadBytes(m_current_block_encrypted.data() + PART_1_SIZE, PART_2_SIZE))
{
file_2.ClearError();
return false;
}
}
else
{
// Normal case. The read is offset by 0x200 bytes, but it's all within one file.

File::IOFile& file = m_files[file_index];

if (!file.Seek(sizeof(NFSHeader) + block_in_file * BLOCK_SIZE, File::SeekOrigin::Begin) ||
!file.ReadBytes(m_current_block_encrypted.data(), BLOCK_SIZE))
{
file.ClearError();
return false;
}
}

return true;
}

void NFSFileReader::DecryptBlock(u64 logical_block_index)
{
std::array<u8, 16> iv{};
const u64 swapped_block_index = Common::swap64(logical_block_index);
std::memcpy(iv.data() + iv.size() - sizeof(swapped_block_index), &swapped_block_index,
sizeof(swapped_block_index));

m_aes_context->Crypt(iv.data(), m_current_block_encrypted.data(),
m_current_block_decrypted.data(), BLOCK_SIZE);
}

bool NFSFileReader::ReadAndDecryptBlock(u64 logical_block_index)
{
const u64 physical_block_index = ToPhysicalBlockIndex(logical_block_index);

if (physical_block_index == std::numeric_limits<u64>::max())
{
// The block isn't physically present. Treat its contents as all zeroes.
m_current_block_decrypted.fill(0);
}
else
{
if (!ReadEncryptedBlock(physical_block_index))
return false;

DecryptBlock(logical_block_index);
}

// Small hack: Set 0x61 of the header to 1 so that VolumeWii realizes that the disc is unencrypted
if (logical_block_index == 0)
m_current_block_decrypted[0x61] = 1;

return true;
}

bool NFSFileReader::Read(u64 offset, u64 nbytes, u8* out_ptr)
{
while (nbytes != 0)
{
const u64 logical_block_index = offset / BLOCK_SIZE;
const u64 offset_in_block = offset % BLOCK_SIZE;

if (logical_block_index != m_current_logical_block_index)
{
if (!ReadAndDecryptBlock(logical_block_index))
return false;

m_current_logical_block_index = logical_block_index;
}

const u64 bytes_to_copy = std::min(nbytes, BLOCK_SIZE - offset_in_block);
std::memcpy(out_ptr, m_current_block_decrypted.data() + offset_in_block, bytes_to_copy);

offset += bytes_to_copy;
nbytes -= bytes_to_copy;
out_ptr += bytes_to_copy;
}

return true;
}

} // namespace DiscIO
91 changes: 91 additions & 0 deletions Source/Core/DiscIO/NFSBlob.h
@@ -0,0 +1,91 @@
// Copyright 2022 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#pragma once

#include <array>
#include <limits>
#include <memory>
#include <string>
#include <vector>

#include "Common/CommonTypes.h"
#include "Common/Crypto/AES.h"
#include "Common/IOFile.h"
#include "DiscIO/Blob.h"

// This is the file format used for Wii games released on the Wii U eShop.

namespace DiscIO
{
static constexpr u32 NFS_MAGIC = 0x53474745; // "EGGS" (byteswapped to little endian)

struct NFSLBARange
{
u32 start_block;
u32 num_blocks;
};

struct NFSHeader
{
u32 magic; // EGGS
u32 version;
u32 unknown_1;
u32 unknown_2;
u32 lba_range_count;
std::array<NFSLBARange, 61> lba_ranges;
u32 end_magic; // SGGE
};
static_assert(sizeof(NFSHeader) == 0x200);

class NFSFileReader : public BlobReader
{
public:
static std::unique_ptr<NFSFileReader> Create(File::IOFile first_file,
const std::string& directory_path);

BlobType GetBlobType() const override { return BlobType::NFS; }

u64 GetRawSize() const override;
u64 GetDataSize() const override;
DataSizeType GetDataSizeType() const override { return DataSizeType::LowerBound; }

u64 GetBlockSize() const override { return BLOCK_SIZE; }
bool HasFastRandomAccessInBlock() const override { return false; }
std::string GetCompressionMethod() const override { return {}; }
std::optional<int> GetCompressionLevel() const override { return std::nullopt; }

bool Read(u64 offset, u64 nbytes, u8* out_ptr) override;

private:
using Key = std::array<u8, Common::AES::Context::KEY_SIZE>;
static constexpr u32 BLOCK_SIZE = 0x8000;
static constexpr u32 MAX_FILE_SIZE = 0xFA00000;

static bool ReadKey(const std::string& path, const std::string& directory, Key* key_out);
static std::vector<NFSLBARange> GetLBARanges(const NFSHeader& header);
static std::vector<File::IOFile> OpenFiles(const std::string& directory, File::IOFile first_file,
u64 expected_raw_size, u64* raw_size_out);
static u64 CalculateExpectedRawSize(const std::vector<NFSLBARange>& lba_ranges);
static u64 CalculateExpectedDataSize(const std::vector<NFSLBARange>& lba_ranges);

NFSFileReader(std::vector<NFSLBARange> lba_ranges, std::vector<File::IOFile> files, Key key,
u64 raw_size);

u64 ToPhysicalBlockIndex(u64 logical_block_index);
bool ReadEncryptedBlock(u64 physical_block_index);
void DecryptBlock(u64 logical_block_index);
bool ReadAndDecryptBlock(u64 logical_block_index);

std::array<u8, BLOCK_SIZE> m_current_block_encrypted;
std::array<u8, BLOCK_SIZE> m_current_block_decrypted;
u64 m_current_logical_block_index = std::numeric_limits<u64>::max();

std::vector<NFSLBARange> m_lba_ranges;
std::vector<File::IOFile> m_files;
std::unique_ptr<Common::AES::Context> m_aes_context;
u64 m_raw_size;
u64 m_data_size;
};

} // namespace DiscIO
2 changes: 1 addition & 1 deletion Source/Core/DiscIO/ScrubbedBlob.h
Expand Up @@ -22,7 +22,7 @@ class ScrubbedBlob : public BlobReader

u64 GetRawSize() const override { return m_blob_reader->GetRawSize(); }
u64 GetDataSize() const override { return m_blob_reader->GetDataSize(); }
bool IsDataSizeAccurate() const override { return m_blob_reader->IsDataSizeAccurate(); }
DataSizeType GetDataSizeType() const override { return m_blob_reader->GetDataSizeType(); }

u64 GetBlockSize() const override { return m_blob_reader->GetBlockSize(); }
bool HasFastRandomAccessInBlock() const override
Expand Down
2 changes: 1 addition & 1 deletion Source/Core/DiscIO/TGCBlob.h
Expand Up @@ -45,7 +45,7 @@ class TGCFileReader final : public BlobReader

u64 GetRawSize() const override { return m_size; }
u64 GetDataSize() const override;
bool IsDataSizeAccurate() const override { return true; }
DataSizeType GetDataSizeType() const override { return DataSizeType::Accurate; }

u64 GetBlockSize() const override { return 0; }
bool HasFastRandomAccessInBlock() const override { return true; }
Expand Down
9 changes: 5 additions & 4 deletions Source/Core/DiscIO/Volume.h
Expand Up @@ -22,6 +22,7 @@ namespace DiscIO
{
class BlobReader;
enum class BlobType;
enum class DataSizeType;
class FileSystem;
class VolumeDisc;
class VolumeWAD;
Expand Down Expand Up @@ -63,7 +64,8 @@ class Volume
return static_cast<u64>(*temp) << GetOffsetShift();
}

virtual bool IsEncryptedAndHashed() const { return false; }
virtual bool HasWiiHashes() const { return false; }
virtual bool HasWiiEncryption() const { return false; }
virtual std::vector<Partition> GetPartitions() const { return {}; }
virtual Partition GetGamePartition() const { return PARTITION_NONE; }
virtual std::optional<u32> GetPartitionType(const Partition& partition) const
Expand Down Expand Up @@ -122,7 +124,6 @@ class Volume
virtual Platform GetVolumeType() const = 0;
virtual bool IsDatelDisc() const = 0;
virtual bool IsNKit() const = 0;
virtual bool SupportsIntegrityCheck() const { return false; }
virtual bool CheckH3TableIntegrity(const Partition& partition) const { return false; }
virtual bool CheckBlockIntegrity(u64 block_index, const u8* encrypted_data,
const Partition& partition) const
Expand All @@ -137,8 +138,8 @@ class Volume
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;
virtual u64 GetDataSize() const = 0;
virtual DataSizeType GetDataSizeType() const = 0;
// Size on disc (compressed size)
virtual u64 GetRawSize() const = 0;
virtual const BlobReader& GetBlobReader() const = 0;
Expand Down
2 changes: 1 addition & 1 deletion Source/Core/DiscIO/VolumeFileBlobReader.h
Expand Up @@ -25,7 +25,7 @@ class VolumeFileBlobReader final : public BlobReader

u64 GetRawSize() const override;
u64 GetDataSize() const override;
bool IsDataSizeAccurate() const override { return true; }
DataSizeType GetDataSizeType() const override { return DataSizeType::Accurate; }

u64 GetBlockSize() const override;
bool HasFastRandomAccessInBlock() const override;
Expand Down
6 changes: 3 additions & 3 deletions Source/Core/DiscIO/VolumeGC.cpp
Expand Up @@ -119,14 +119,14 @@ BlobType VolumeGC::GetBlobType() const
return m_reader->GetBlobType();
}

u64 VolumeGC::GetSize() const
u64 VolumeGC::GetDataSize() const
{
return m_reader->GetDataSize();
}

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

u64 VolumeGC::GetRawSize() const
Expand Down
4 changes: 2 additions & 2 deletions Source/Core/DiscIO/VolumeGC.h
Expand Up @@ -45,8 +45,8 @@ class VolumeGC : public VolumeDisc
bool IsDatelDisc() const override;
Region GetRegion() const override;
BlobType GetBlobType() const override;
u64 GetSize() const override;
bool IsSizeAccurate() const override;
u64 GetDataSize() const override;
DataSizeType GetDataSizeType() const override;
u64 GetRawSize() const override;
const BlobReader& GetBlobReader() const override;

Expand Down
80 changes: 54 additions & 26 deletions Source/Core/DiscIO/VolumeVerifier.cpp
Expand Up @@ -62,7 +62,7 @@ void RedumpVerifier::Start(const Volume& volume)

m_revision = volume.GetRevision().value_or(0);
m_disc_number = volume.GetDiscNumber().value_or(0);
m_size = volume.GetSize();
m_size = volume.GetDataSize();

const DiscIO::Platform platform = volume.GetVolumeType();

Expand Down Expand Up @@ -364,7 +364,7 @@ VolumeVerifier::VolumeVerifier(const Volume& volume, bool redump_verification,
m_hashes_to_calculate(hashes_to_calculate),
m_calculating_any_hash(hashes_to_calculate.crc32 || hashes_to_calculate.md5 ||
hashes_to_calculate.sha1),
m_max_progress(volume.GetSize())
m_max_progress(volume.GetDataSize()), m_data_size_type(volume.GetDataSizeType())
{
if (!m_calculating_any_hash)
m_redump_verification = false;
Expand Down Expand Up @@ -403,9 +403,8 @@ void VolumeVerifier::Start()

m_is_tgc = m_volume.GetBlobType() == BlobType::TGC;
m_is_datel = m_volume.IsDatelDisc();
m_is_not_retail =
(m_volume.GetVolumeType() == Platform::WiiDisc && !m_volume.IsEncryptedAndHashed()) ||
IsDebugSigned();
m_is_not_retail = (m_volume.GetVolumeType() == Platform::WiiDisc && !m_volume.HasWiiHashes()) ||
IsDebugSigned();

const std::vector<Partition> partitions = CheckPartitions();

Expand Down Expand Up @@ -492,7 +491,7 @@ std::vector<Partition> VolumeVerifier::CheckPartitions()
Common::GetStringT("The update partition is not at its normal position."));
}

const u64 normal_data_offset = m_volume.IsEncryptedAndHashed() ? 0xF800000 : 0x838000;
const u64 normal_data_offset = m_volume.HasWiiHashes() ? 0xF800000 : 0x838000;
if (m_volume.GetPartitionType(partition) == PARTITION_DATA &&
partition.offset != normal_data_offset && !has_channel_partition && !has_install_partition)
{
Expand Down Expand Up @@ -593,14 +592,14 @@ bool VolumeVerifier::CheckPartition(const Partition& partition)
}
}

if (m_volume.SupportsIntegrityCheck() && !m_volume.CheckH3TableIntegrity(partition))
if (m_volume.HasWiiHashes() && !m_volume.CheckH3TableIntegrity(partition))
{
AddProblem(Severity::Low,
Common::FmtFormatT("The H3 hash table for the {0} partition is not correct.", name));
}

// Prepare for hash verification in the Process step
if (m_volume.SupportsIntegrityCheck())
if (m_volume.HasWiiHashes())
{
const u64 data_size =
m_volume.ReadSwappedAndShifted(partition.offset + 0x2bc, PARTITION_NONE).value_or(0);
Expand Down Expand Up @@ -759,11 +758,10 @@ bool VolumeVerifier::ShouldBeDualLayer() const

void VolumeVerifier::CheckVolumeSize()
{
u64 volume_size = m_volume.GetSize();
u64 volume_size = m_volume.GetDataSize();
const bool is_disc = IsDisc(m_volume.GetVolumeType());
const bool should_be_dual_layer = is_disc && ShouldBeDualLayer();
const bool is_size_accurate = m_volume.IsSizeAccurate();
bool volume_size_roughly_known = is_size_accurate;
bool volume_size_roughly_known = m_data_size_type != DiscIO::DataSizeType::UpperBound;

if (should_be_dual_layer && m_biggest_referenced_offset <= SL_DVD_R_SIZE)
{
Expand All @@ -774,13 +772,13 @@ void VolumeVerifier::CheckVolumeSize()
"This problem generally only exists in illegal copies of games."));
}

if (!is_size_accurate)
if (m_data_size_type != DiscIO::DataSizeType::Accurate)
{
AddProblem(Severity::Low,
Common::GetStringT("The format that the disc image is saved in does not "
"store the size of the disc image."));

if (m_volume.SupportsIntegrityCheck())
if (!volume_size_roughly_known && m_volume.HasWiiHashes())
{
volume_size = m_biggest_verified_offset;
volume_size_roughly_known = true;
Expand All @@ -804,7 +802,10 @@ void VolumeVerifier::CheckVolumeSize()
return;
}

if (is_disc && is_size_accurate && !m_is_tgc)
// The reason why this condition is checking for m_data_size_type != UpperBound instead of
// m_data_size_type == Accurate is because we want to show the warning about input recordings and
// NetPlay for NFS disc images (which are the only disc images that have it set to LowerBound).
if (is_disc && m_data_size_type != DiscIO::DataSizeType::UpperBound && !m_is_tgc)
{
const Platform platform = m_volume.GetVolumeType();
const bool should_be_gc_size = platform == Platform::GameCubeDisc || m_is_datel;
Expand Down Expand Up @@ -1118,7 +1119,7 @@ void VolumeVerifier::Process()
ASSERT(m_started);
ASSERT(!m_done);

if (m_progress == m_max_progress)
if (m_progress >= m_max_progress)
return;

IOS::ES::Content content{};
Expand Down Expand Up @@ -1166,13 +1167,21 @@ void VolumeVerifier::Process()
if (m_progress + bytes_to_read > m_max_progress)
{
const u64 bytes_over_max = m_progress + bytes_to_read - m_max_progress;
bytes_to_read -= bytes_over_max;
if (excess_bytes < bytes_over_max)
excess_bytes = 0;

if (m_data_size_type == DataSizeType::LowerBound)
{
// Disc images in NFS format can have the last referenced block be past m_max_progress.
// For NFS, reading beyond m_max_progress doesn't return an error, so let's read beyond it.
excess_bytes = std::max(excess_bytes, bytes_over_max);
}
else
excess_bytes -= bytes_over_max;
content_read = false;
group_read = false;
{
// Don't read beyond the end of the disc.
bytes_to_read -= bytes_over_max;
excess_bytes -= std::min(excess_bytes, bytes_over_max);
content_read = false;
group_read = false;
}
}

const bool is_data_needed = m_calculating_any_hash || content_read || group_read;
Expand Down Expand Up @@ -1376,8 +1385,18 @@ void VolumeVerifier::Finish()
if (m_result.redump.status == RedumpVerifier::Status::BadDump &&
highest_severity <= Severity::Low)
{
m_result.summary_text = Common::GetStringT(
"This is a bad dump. This doesn't necessarily mean that the game won't run correctly.");
if (m_volume.GetBlobType() == BlobType::NFS)
{
m_result.summary_text =
Common::GetStringT("Compared to the Wii disc release of the game, this is a bad dump. "
"Despite this, it's possible that this is a good dump compared to the "
"Wii U eShop release of the game. Dolphin can't verify this.");
}
else
{
m_result.summary_text = Common::GetStringT(
"This is a bad dump. This doesn't necessarily mean that the game won't run correctly.");
}
}
else
{
Expand All @@ -1402,9 +1421,18 @@ void VolumeVerifier::Finish()
}
break;
case Severity::Low:
m_result.summary_text =
Common::GetStringT("Problems with low severity were found. They will most "
"likely not prevent the game from running.");
if (m_volume.GetBlobType() == BlobType::NFS)
{
m_result.summary_text = Common::GetStringT(
"Compared to the Wii disc release of the game, problems of low severity were found. "
"Despite this, it's possible that this is a good dump compared to the Wii U eShop "
"release of the game. Dolphin can't verify this.");
}
else
{
Common::GetStringT("Problems with low severity were found. They will most "
"likely not prevent the game from running.");
}
break;
case Severity::Medium:
m_result.summary_text +=
Expand Down
1 change: 1 addition & 0 deletions Source/Core/DiscIO/VolumeVerifier.h
Expand Up @@ -202,6 +202,7 @@ class VolumeVerifier final
bool m_done = false;
u64 m_progress = 0;
u64 m_max_progress = 0;
DataSizeType m_data_size_type;
};

} // namespace DiscIO
6 changes: 3 additions & 3 deletions Source/Core/DiscIO/VolumeWad.cpp
Expand Up @@ -318,14 +318,14 @@ BlobType VolumeWAD::GetBlobType() const
return m_reader->GetBlobType();
}

u64 VolumeWAD::GetSize() const
u64 VolumeWAD::GetDataSize() const
{
return m_reader->GetDataSize();
}

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

u64 VolumeWAD::GetRawSize() const
Expand Down
4 changes: 2 additions & 2 deletions Source/Core/DiscIO/VolumeWad.h
Expand Up @@ -64,8 +64,8 @@ class VolumeWAD : public Volume
Country GetCountry(const Partition& partition = PARTITION_NONE) const override;

BlobType GetBlobType() const override;
u64 GetSize() const override;
bool IsSizeAccurate() const override;
u64 GetDataSize() const override;
DataSizeType GetDataSizeType() const override;
u64 GetRawSize() const override;
const BlobReader& GetBlobReader() const override;

Expand Down
109 changes: 76 additions & 33 deletions Source/Core/DiscIO/VolumeWii.cpp
Expand Up @@ -41,7 +41,11 @@ VolumeWii::VolumeWii(std::unique_ptr<BlobReader> reader)
{
ASSERT(m_reader);

m_encrypted = m_reader->ReadSwapped<u32>(0x60) == u32(0);
m_has_hashes = m_reader->ReadSwapped<u8>(0x60) == u8(0);
m_has_encryption = m_reader->ReadSwapped<u8>(0x61) == u8(0);

if (m_has_encryption && !m_has_hashes)
ERROR_LOG_FMT(DISCIO, "Wii disc has encryption but no hashes! This probably won't work well");

for (u32 partition_group = 0; partition_group < 4; ++partition_group)
{
Expand Down Expand Up @@ -114,7 +118,7 @@ VolumeWii::VolumeWii(std::unique_ptr<BlobReader> reader)
};

auto get_h3_table = [this, partition]() -> std::vector<u8> {
if (!m_encrypted)
if (!m_has_hashes)
return {};
const std::optional<u64> h3_table_offset = ReadSwappedAndShifted(
partition.offset + WII_PARTITION_H3_OFFSET_ADDRESS, PARTITION_NONE);
Expand Down Expand Up @@ -170,35 +174,55 @@ bool VolumeWii::Read(u64 offset, u64 length, u8* buffer, const Partition& partit
const PartitionDetails& partition_details = it->second;

const u64 partition_data_offset = partition.offset + *partition_details.data_offset;
if (m_reader->SupportsReadWiiDecrypted(offset, length, partition_data_offset))
if (m_has_hashes && m_has_encryption &&
m_reader->SupportsReadWiiDecrypted(offset, length, partition_data_offset))
{
return m_reader->ReadWiiDecrypted(offset, length, buffer, partition_data_offset);
}

if (!m_encrypted)
if (!m_has_hashes)
{
return m_reader->Read(partition.offset + *partition_details.data_offset + offset, length,
buffer);
return m_reader->Read(partition_data_offset + offset, length, buffer);
}

auto aes_context = partition_details.key->get();
if (!aes_context)
return false;
Common::AES::Context* aes_context = nullptr;
std::unique_ptr<u8[]> read_buffer = nullptr;
if (m_has_encryption)
{
aes_context = partition_details.key->get();
if (!aes_context)
return false;

read_buffer = std::make_unique<u8[]>(BLOCK_TOTAL_SIZE);
}

auto read_buffer = std::make_unique<u8[]>(BLOCK_TOTAL_SIZE);
while (length > 0)
{
// Calculate offsets
u64 block_offset_on_disc = partition.offset + *partition_details.data_offset +
offset / BLOCK_DATA_SIZE * BLOCK_TOTAL_SIZE;
u64 block_offset_on_disc = partition_data_offset + offset / BLOCK_DATA_SIZE * BLOCK_TOTAL_SIZE;
u64 data_offset_in_block = offset % BLOCK_DATA_SIZE;

if (m_last_decrypted_block != block_offset_on_disc)
{
// Read the current block
if (!m_reader->Read(block_offset_on_disc, BLOCK_TOTAL_SIZE, read_buffer.get()))
return false;
if (m_has_encryption)
{
// Read the current block
if (!m_reader->Read(block_offset_on_disc, BLOCK_TOTAL_SIZE, read_buffer.get()))
return false;

// Decrypt the block's data
DecryptBlockData(read_buffer.get(), m_last_decrypted_block_data, aes_context);
}
else
{
// Read the current block
if (!m_reader->Read(block_offset_on_disc + BLOCK_HEADER_SIZE, BLOCK_DATA_SIZE,
m_last_decrypted_block_data))
{
return false;
}
}

// Decrypt the block's data
DecryptBlockData(read_buffer.get(), m_last_decrypted_block_data, aes_context);
m_last_decrypted_block = block_offset_on_disc;
}

Expand All @@ -216,9 +240,14 @@ bool VolumeWii::Read(u64 offset, u64 length, u8* buffer, const Partition& partit
return true;
}

bool VolumeWii::IsEncryptedAndHashed() const
bool VolumeWii::HasWiiHashes() const
{
return m_encrypted;
return m_has_hashes;
}

bool VolumeWii::HasWiiEncryption() const
{
return m_has_encryption;
}

std::vector<Partition> VolumeWii::GetPartitions() const
Expand Down Expand Up @@ -272,8 +301,8 @@ const FileSystem* VolumeWii::GetFileSystem(const Partition& partition) const
return it != m_partitions.end() ? it->second.file_system->get() : nullptr;
}

u64 VolumeWii::EncryptedPartitionOffsetToRawOffset(u64 offset, const Partition& partition,
u64 partition_data_offset)
u64 VolumeWii::OffsetInHashedPartitionToRawOffset(u64 offset, const Partition& partition,
u64 partition_data_offset)
{
if (partition == PARTITION_NONE)
return offset;
Expand All @@ -289,10 +318,10 @@ u64 VolumeWii::PartitionOffsetToRawOffset(u64 offset, const Partition& partition
return offset;
const u64 data_offset = *it->second.data_offset;

if (!m_encrypted)
if (!m_has_hashes)
return partition.offset + data_offset + offset;

return EncryptedPartitionOffsetToRawOffset(offset, partition, data_offset);
return OffsetInHashedPartitionToRawOffset(offset, partition, data_offset);
}

std::string VolumeWii::GetGameTDBID(const Partition& partition) const
Expand Down Expand Up @@ -340,14 +369,14 @@ BlobType VolumeWii::GetBlobType() const
return m_reader->GetBlobType();
}

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

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

u64 VolumeWii::GetRawSize() const
Expand Down Expand Up @@ -415,23 +444,37 @@ bool VolumeWii::CheckBlockIntegrity(u64 block_index, const u8* encrypted_data,

if (block_index / BLOCKS_PER_GROUP * Common::SHA1::DIGEST_LEN >=
partition_details.h3_table->size())
{
return false;

auto aes_context = partition_details.key->get();
if (!aes_context)
return false;
}

HashBlock hashes;
DecryptBlockHashes(encrypted_data, &hashes, aes_context);
u8 cluster_data_buffer[BLOCK_DATA_SIZE];
const u8* cluster_data;

auto cluster_data = std::make_unique<u8[]>(BLOCK_DATA_SIZE);
DecryptBlockData(encrypted_data, cluster_data.get(), aes_context);
if (m_has_encryption)
{
Common::AES::Context* aes_context = partition_details.key->get();
if (!aes_context)
return false;

DecryptBlockHashes(encrypted_data, &hashes, aes_context);
DecryptBlockData(encrypted_data, cluster_data_buffer, aes_context);
cluster_data = cluster_data_buffer;
}
else
{
std::memcpy(&hashes, encrypted_data, BLOCK_HEADER_SIZE);
cluster_data = encrypted_data + BLOCK_HEADER_SIZE;
}

for (u32 hash_index = 0; hash_index < 31; ++hash_index)
{
if (Common::SHA1::CalculateDigest(&cluster_data[hash_index * 0x400], 0x400) !=
hashes.h0[hash_index])
{
return false;
}
}

if (Common::SHA1::CalculateDigest(hashes.h0) != hashes.h1[block_index % 8])
Expand Down
15 changes: 8 additions & 7 deletions Source/Core/DiscIO/VolumeWii.h
Expand Up @@ -60,7 +60,8 @@ class VolumeWii : public VolumeDisc
VolumeWii(std::unique_ptr<BlobReader> reader);
~VolumeWii();
bool Read(u64 offset, u64 length, u8* buffer, const Partition& partition) const override;
bool IsEncryptedAndHashed() const override;
bool HasWiiHashes() const override;
bool HasWiiEncryption() const override;
std::vector<Partition> GetPartitions() const override;
Partition GetGamePartition() const override;
std::optional<u32> GetPartitionType(const Partition& partition) const override;
Expand All @@ -69,25 +70,24 @@ class VolumeWii : public VolumeDisc
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);
static u64 OffsetInHashedPartitionToRawOffset(u64 offset, const Partition& partition,
u64 partition_data_offset);
u64 PartitionOffsetToRawOffset(u64 offset, const Partition& partition) const override;
std::string GetGameTDBID(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;

Platform GetVolumeType() const override;
bool IsDatelDisc() const override;
bool SupportsIntegrityCheck() const override { return m_encrypted; }
bool CheckH3TableIntegrity(const Partition& partition) const override;
bool CheckBlockIntegrity(u64 block_index, const u8* encrypted_data,
const Partition& partition) const override;
bool CheckBlockIntegrity(u64 block_index, const Partition& partition) const override;

Region GetRegion() const override;
BlobType GetBlobType() const override;
u64 GetSize() const override;
bool IsSizeAccurate() const override;
u64 GetDataSize() const override;
DataSizeType GetDataSizeType() const override;
u64 GetRawSize() const override;
const BlobReader& GetBlobReader() const override;
std::array<u8, 20> GetSyncHash() const override;
Expand Down Expand Up @@ -128,7 +128,8 @@ class VolumeWii : public VolumeDisc
std::unique_ptr<BlobReader> m_reader;
std::map<Partition, PartitionDetails> m_partitions;
Partition m_game_partition;
bool m_encrypted;
bool m_has_hashes;
bool m_has_encryption;

mutable u64 m_last_decrypted_block;
mutable u8 m_last_decrypted_block_data[BLOCK_DATA_SIZE]{};
Expand Down
4 changes: 2 additions & 2 deletions Source/Core/DiscIO/WIABlob.cpp
Expand Up @@ -925,7 +925,7 @@ ConversionResultCode WIARVZFileReader<RVZ>::SetUpDataEntriesForWriting(
std::vector<DataEntry>* data_entries, std::vector<const FileSystem*>* partition_file_systems)
{
std::vector<Partition> partitions;
if (volume && volume->IsEncryptedAndHashed())
if (volume && volume->HasWiiHashes() && volume->HasWiiEncryption())
partitions = volume->GetPartitions();

std::sort(partitions.begin(), partitions.end(),
Expand Down Expand Up @@ -1731,7 +1731,7 @@ WIARVZFileReader<RVZ>::Convert(BlobReader* infile, const VolumeDisc* infile_volu
File::IOFile* outfile, WIARVZCompressionType compression_type,
int compression_level, int chunk_size, CompressCB callback)
{
ASSERT(infile->IsDataSizeAccurate());
ASSERT(infile->GetDataSizeType() == DataSizeType::Accurate);
ASSERT(chunk_size > 0);

const u64 iso_size = infile->GetDataSize();
Expand Down
2 changes: 1 addition & 1 deletion Source/Core/DiscIO/WIABlob.h
Expand Up @@ -52,7 +52,7 @@ class WIARVZFileReader : public BlobReader

u64 GetRawSize() const override { return Common::swap64(m_header_1.wia_file_size); }
u64 GetDataSize() const override { return Common::swap64(m_header_1.iso_file_size); }
bool IsDataSizeAccurate() const override { return true; }
DataSizeType GetDataSizeType() const override { return DataSizeType::Accurate; }

u64 GetBlockSize() const override { return Common::swap32(m_header_2.chunk_size); }
bool HasFastRandomAccessInBlock() const override { return false; }
Expand Down
5 changes: 1 addition & 4 deletions Source/Core/DiscIO/WbfsBlob.h
Expand Up @@ -25,11 +25,8 @@ class WbfsFileReader : public BlobReader
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; }
DataSizeType GetDataSizeType() const override { return DataSizeType::UpperBound; }

u64 GetBlockSize() const override { return m_wbfs_sector_size; }
bool HasFastRandomAccessInBlock() const override { return true; }
Expand Down
2 changes: 2 additions & 0 deletions Source/Core/DolphinLib.props
Expand Up @@ -442,6 +442,7 @@
<ClInclude Include="DiscIO\LaggedFibonacciGenerator.h" />
<ClInclude Include="DiscIO\MultithreadedCompressor.h" />
<ClInclude Include="DiscIO\NANDImporter.h" />
<ClInclude Include="DiscIO\NFSBlob.h" />
<ClInclude Include="DiscIO\RiivolutionParser.h" />
<ClInclude Include="DiscIO\RiivolutionPatcher.h" />
<ClInclude Include="DiscIO\ScrubbedBlob.h" />
Expand Down Expand Up @@ -1056,6 +1057,7 @@
<ClCompile Include="DiscIO\GameModDescriptor.cpp" />
<ClCompile Include="DiscIO\LaggedFibonacciGenerator.cpp" />
<ClCompile Include="DiscIO\NANDImporter.cpp" />
<ClCompile Include="DiscIO\NFSBlob.cpp" />
<ClCompile Include="DiscIO\RiivolutionParser.cpp" />
<ClCompile Include="DiscIO\RiivolutionPatcher.cpp" />
<ClCompile Include="DiscIO\ScrubbedBlob.cpp" />
Expand Down
13 changes: 7 additions & 6 deletions Source/Core/DolphinQt/GameList/GameTracker.cpp
Expand Up @@ -22,12 +22,13 @@

// NOTE: Qt likes to be case-sensitive here even though it shouldn't be thus this ugly regex hack
static const QStringList game_filters{
QStringLiteral("*.[gG][cC][mM]"), QStringLiteral("*.[iI][sS][oO]"),
QStringLiteral("*.[tT][gG][cC]"), QStringLiteral("*.[cC][iI][sS][oO]"),
QStringLiteral("*.[gG][cC][zZ]"), QStringLiteral("*.[wW][bB][fF][sS]"),
QStringLiteral("*.[wW][iI][aA]"), QStringLiteral("*.[rR][vV][zZ]"),
QStringLiteral("*.[wW][aA][dD]"), QStringLiteral("*.[eE][lL][fF]"),
QStringLiteral("*.[dD][oO][lL]"), QStringLiteral("*.[jJ][sS][oO][nN]")};
QStringLiteral("*.[gG][cC][mM]"), QStringLiteral("*.[iI][sS][oO]"),
QStringLiteral("*.[tT][gG][cC]"), QStringLiteral("*.[cC][iI][sS][oO]"),
QStringLiteral("*.[gG][cC][zZ]"), QStringLiteral("*.[wW][bB][fF][sS]"),
QStringLiteral("*.[wW][iI][aA]"), QStringLiteral("*.[rR][vV][zZ]"),
QStringLiteral("hif_000000.nfs"), QStringLiteral("*.[wW][aA][dD]"),
QStringLiteral("*.[eE][lL][fF]"), QStringLiteral("*.[dD][oO][lL]"),
QStringLiteral("*.[jJ][sS][oO][nN]")};

GameTracker::GameTracker(QObject* parent) : QFileSystemWatcher(parent)
{
Expand Down
1 change: 1 addition & 0 deletions Source/Core/DolphinQt/Info.plist.in
Expand Up @@ -14,6 +14,7 @@
<string>gcz</string>
<string>iso</string>
<string>m3u</string>
<string>nfs</string>
<string>rvz</string>
<string>tgc</string>
<string>wad</string>
Expand Down
4 changes: 2 additions & 2 deletions Source/Core/DolphinQt/MainWindow.cpp
Expand Up @@ -725,8 +725,8 @@ QStringList MainWindow::PromptFileNames()
QStringList paths = DolphinFileDialog::getOpenFileNames(
this, tr("Select a File"),
settings.value(QStringLiteral("mainwindow/lastdir"), QString{}).toString(),
QStringLiteral("%1 (*.elf *.dol *.gcm *.iso *.tgc *.wbfs *.ciso *.gcz *.wia *.rvz *.wad "
"*.dff *.m3u *.json);;%2 (*)")
QStringLiteral("%1 (*.elf *.dol *.gcm *.iso *.tgc *.wbfs *.ciso *.gcz *.wia *.rvz "
"hif_000000.nfs *.wad *.dff *.m3u *.json);;%2 (*)")
.arg(tr("All GC/Wii files"))
.arg(tr("All Files")));

Expand Down
4 changes: 2 additions & 2 deletions Source/Core/DolphinQt/Settings/PathPane.cpp
Expand Up @@ -45,8 +45,8 @@ void PathPane::BrowseDefaultGame()
{
QString file = QDir::toNativeSeparators(DolphinFileDialog::getOpenFileName(
this, tr("Select a Game"), Settings::Instance().GetDefaultGame(),
QStringLiteral("%1 (*.elf *.dol *.gcm *.iso *.tgc *.wbfs *.ciso *.gcz *.wia *.rvz *.wad "
"*.m3u *.json);;%2 (*)")
QStringLiteral("%1 (*.elf *.dol *.gcm *.iso *.tgc *.wbfs *.ciso *.gcz *.wia *.rvz "
"hif_000000.nfs *.wad *.m3u *.json);;%2 (*)")
.arg(tr("All GC/Wii files"))
.arg(tr("All Files"))));

Expand Down
2 changes: 1 addition & 1 deletion Source/Core/DolphinTool/ConvertCommand.cpp
Expand Up @@ -212,7 +212,7 @@ int ConvertCommand::Main(const std::vector<std::string>& args)
}

if (format == DiscIO::BlobType::GCZ && volume &&
!DiscIO::IsGCZBlockSizeLegacyCompatible(block_size_o.value(), volume->GetSize()))
!DiscIO::IsGCZBlockSizeLegacyCompatible(block_size_o.value(), volume->GetDataSize()))
{
std::cerr << "Warning: For GCZs to be compatible with Dolphin < 5.0-11893, "
"the file size must be an integer multiple of the block size "
Expand Down
10 changes: 5 additions & 5 deletions Source/Core/UICommon/GameFile.cpp
Expand Up @@ -133,8 +133,8 @@ GameFile::GameFile(std::string path) : m_file_path(std::move(path))
m_block_size = volume->GetBlobReader().GetBlockSize();
m_compression_method = volume->GetBlobReader().GetCompressionMethod();
m_file_size = volume->GetRawSize();
m_volume_size = volume->GetSize();
m_volume_size_is_accurate = volume->IsSizeAccurate();
m_volume_size = volume->GetDataSize();
m_volume_size_type = volume->GetDataSizeType();
m_is_datel_disc = volume->IsDatelDisc();
m_is_nkit = volume->IsNKit();

Expand All @@ -158,7 +158,7 @@ GameFile::GameFile(std::string path) : m_file_path(std::move(path))
m_valid = true;
m_file_size = m_volume_size = File::GetSize(m_file_path);
m_game_id = SConfig::MakeGameID(m_file_name);
m_volume_size_is_accurate = true;
m_volume_size_type = DiscIO::DataSizeType::Accurate;
m_is_datel_disc = false;
m_is_nkit = false;
m_platform = DiscIO::Platform::ELFOrDOL;
Expand Down Expand Up @@ -349,7 +349,7 @@ void GameFile::DoState(PointerWrap& p)

p.Do(m_file_size);
p.Do(m_volume_size);
p.Do(m_volume_size_is_accurate);
p.Do(m_volume_size_type);
p.Do(m_is_datel_disc);
p.Do(m_is_nkit);

Expand Down Expand Up @@ -827,7 +827,7 @@ std::string GameFile::GetFileFormatName() const

bool GameFile::ShouldAllowConversion() const
{
return DiscIO::IsDisc(m_platform) && m_volume_size_is_accurate;
return DiscIO::IsDisc(m_platform) && m_volume_size_type == DiscIO::DataSizeType::Accurate;
}

bool GameFile::IsModDescriptor() const
Expand Down
4 changes: 2 additions & 2 deletions Source/Core/UICommon/GameFile.h
Expand Up @@ -104,7 +104,7 @@ class GameFile final
const std::string& GetApploaderDate() const { return m_apploader_date; }
u64 GetFileSize() const { return m_file_size; }
u64 GetVolumeSize() const { return m_volume_size; }
bool IsVolumeSizeAccurate() const { return m_volume_size_is_accurate; }
DiscIO::DataSizeType GetVolumeSizeType() const { return m_volume_size_type; }
bool IsDatelDisc() const { return m_is_datel_disc; }
bool IsNKit() const { return m_is_nkit; }
bool IsModDescriptor() const;
Expand Down Expand Up @@ -145,7 +145,7 @@ class GameFile final

u64 m_file_size{};
u64 m_volume_size{};
bool m_volume_size_is_accurate{};
DiscIO::DataSizeType m_volume_size_type{};
bool m_is_datel_disc{};
bool m_is_nkit{};

Expand Down
8 changes: 4 additions & 4 deletions Source/Core/UICommon/GameFileCache.cpp
Expand Up @@ -27,14 +27,14 @@

namespace UICommon
{
static constexpr u32 CACHE_REVISION = 21; // Last changed in PR 10187
static constexpr u32 CACHE_REVISION = 23; // Last changed in PR 10932

std::vector<std::string> FindAllGamePaths(const std::vector<std::string>& directories_to_scan,
bool recursive_scan)
{
static const std::vector<std::string> search_extensions = {".gcm", ".tgc", ".iso", ".ciso",
".gcz", ".wbfs", ".wia", ".rvz",
".wad", ".dol", ".elf", ".json"};
static const std::vector<std::string> search_extensions = {
".gcm", ".tgc", ".iso", ".ciso", ".gcz", ".wbfs", ".wia",
".rvz", ".nfs", ".wad", ".dol", ".elf", ".json"};

// TODO: We could process paths iteratively as they are found
return Common::DoFileSearch(directories_to_scan, search_extensions, recursive_scan);
Expand Down