diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.java index 388ed98b69..6924fd65ea 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.java @@ -30,7 +30,8 @@ import java.util.Set; public final class FileBrowserHelper { public static final HashSet GAME_EXTENSIONS = new HashSet<>(Arrays.asList( - "gcm", "tgc", "iso", "ciso", "gcz", "wbfs", "wia", "rvz", "wad", "dol", "elf", "json")); + "gcm", "tgc", "iso", "ciso", "gcz", "wbfs", "wia", "rvz", "nfs", "wad", "dol", "elf", + "json")); public static final HashSet GAME_LIKE_EXTENSIONS = new HashSet<>(GAME_EXTENSIONS); diff --git a/Source/Core/Core/Boot/Boot.cpp b/Source/Core/Core/Boot/Boot.cpp index ba1e901d61..387d628e66 100644 --- a/Source/Core/Core/Boot/Boot.cpp +++ b/Source/Core/Core/Boot/Boot.cpp @@ -231,7 +231,7 @@ std::unique_ptr BootParameters::GenerateFromFile(std::vector disc_image_extensions = { - {".gcm", ".iso", ".tgc", ".wbfs", ".ciso", ".gcz", ".wia", ".rvz", ".dol", ".elf"}}; + {".gcm", ".iso", ".tgc", ".wbfs", ".ciso", ".gcz", ".wia", ".rvz", ".nfs", ".dol", ".elf"}}; if (disc_image_extensions.find(extension) != disc_image_extensions.end() || is_drive) { std::unique_ptr disc = DiscIO::CreateDisc(path); diff --git a/Source/Core/Core/HW/DVD/DVDInterface.cpp b/Source/Core/Core/HW/DVD/DVDInterface.cpp index 8f00b955d2..bac1652fa0 100644 --- a/Source/Core/Core/HW/DVD/DVDInterface.cpp +++ b/Source/Core/Core/HW/DVD/DVDInterface.cpp @@ -430,9 +430,9 @@ void Shutdown() static u64 GetDiscEndOffset(const DiscIO::VolumeDisc& disc) { - u64 size = disc.GetSize(); + u64 size = disc.GetDataSize(); - if (disc.IsSizeAccurate()) + if (disc.GetDataSizeType() == DiscIO::DataSizeType::Accurate) { if (size == DiscIO::MINI_DVD_SIZE) return DiscIO::MINI_DVD_SIZE; @@ -464,7 +464,7 @@ void SetDisc(std::unique_ptr disc, if (has_disc) { s_disc_end_offset = GetDiscEndOffset(*disc); - if (!disc->IsSizeAccurate()) + if (disc->GetDataSizeType() != DiscIO::DataSizeType::Accurate) WARN_LOG_FMT(DVDINTERFACE, "Unknown disc size, guessing {0} bytes", s_disc_end_offset); const DiscIO::BlobReader& blob = disc->GetBlobReader(); @@ -1482,10 +1482,9 @@ static void ScheduleReads(u64 offset, u32 length, const DiscIO::Partition& parti u32 buffered_blocks = 0; u32 unbuffered_blocks = 0; - const u32 bytes_per_chunk = - partition != DiscIO::PARTITION_NONE && DVDThread::IsEncryptedAndHashed() ? - DiscIO::VolumeWii::BLOCK_DATA_SIZE : - DVD_ECC_BLOCK_SIZE; + const u32 bytes_per_chunk = partition != DiscIO::PARTITION_NONE && DVDThread::HasWiiHashes() ? + DiscIO::VolumeWii::BLOCK_DATA_SIZE : + DVD_ECC_BLOCK_SIZE; do { diff --git a/Source/Core/Core/HW/DVD/DVDThread.cpp b/Source/Core/Core/HW/DVD/DVDThread.cpp index 4d37f823ec..5590f58b6e 100644 --- a/Source/Core/Core/HW/DVD/DVDThread.cpp +++ b/Source/Core/Core/HW/DVD/DVDThread.cpp @@ -184,10 +184,10 @@ bool HasDisc() return s_disc != nullptr; } -bool IsEncryptedAndHashed() +bool HasWiiHashes() { - // IsEncryptedAndHashed is thread-safe, so calling WaitUntilIdle isn't necessary. - return s_disc->IsEncryptedAndHashed(); + // HasWiiHashes is thread-safe, so calling WaitUntilIdle isn't necessary. + return s_disc->HasWiiHashes(); } DiscIO::Platform GetDiscType() diff --git a/Source/Core/Core/HW/DVD/DVDThread.h b/Source/Core/Core/HW/DVD/DVDThread.h index 420ab3a3d8..fe427e2e57 100644 --- a/Source/Core/Core/HW/DVD/DVDThread.h +++ b/Source/Core/Core/HW/DVD/DVDThread.h @@ -41,7 +41,7 @@ void DoState(PointerWrap& p); void SetDisc(std::unique_ptr disc); bool HasDisc(); -bool IsEncryptedAndHashed(); +bool HasWiiHashes(); DiscIO::Platform GetDiscType(); u64 PartitionOffsetToRawOffset(u64 offset, const DiscIO::Partition& partition); IOS::ES::TMDReader GetTMD(const DiscIO::Partition& partition); diff --git a/Source/Core/DiscIO/Blob.cpp b/Source/Core/DiscIO/Blob.cpp index 7062c363cd..7c6713e642 100644 --- a/Source/Core/DiscIO/Blob.cpp +++ b/Source/Core/DiscIO/Blob.cpp @@ -20,6 +20,7 @@ #include "DiscIO/DirectoryBlob.h" #include "DiscIO/DriveBlob.h" #include "DiscIO/FileBlob.h" +#include "DiscIO/NFSBlob.h" #include "DiscIO/TGCBlob.h" #include "DiscIO/WIABlob.h" #include "DiscIO/WbfsBlob.h" @@ -52,6 +53,8 @@ std::string GetName(BlobType blob_type, bool translate) return "RVZ"; case BlobType::MOD_DESCRIPTOR: return translate_str("Mod"); + case BlobType::NFS: + return "NFS"; default: return ""; } @@ -242,6 +245,8 @@ std::unique_ptr CreateBlobReader(const std::string& filename) return WIAFileReader::Create(std::move(file), filename); case RVZ_MAGIC: return RVZFileReader::Create(std::move(file), filename); + case NFS_MAGIC: + return NFSFileReader::Create(std::move(file), filename); default: if (auto directory_blob = DirectoryBlobReader::Create(filename)) return std::move(directory_blob); diff --git a/Source/Core/DiscIO/Blob.h b/Source/Core/DiscIO/Blob.h index 03a8644de5..102c151200 100644 --- a/Source/Core/DiscIO/Blob.h +++ b/Source/Core/DiscIO/Blob.h @@ -40,6 +40,19 @@ enum class BlobType WIA, RVZ, MOD_DESCRIPTOR, + NFS, +}; + +// If you convert an ISO file to another format and then call GetDataSize on it, what is the result? +enum class DataSizeType +{ + // The result is the same as for the ISO. + Accurate, + // The result is not larger than for the ISO. (It's usually a little smaller than for the ISO.) + // Reads to offsets that are larger than the result will return some kind of "blank" data. + LowerBound, + // The result is not smaller than for the ISO. (It's usually much larger than for the ISO.) + UpperBound, }; std::string GetName(BlobType blob_type, bool translate); @@ -53,7 +66,7 @@ public: virtual u64 GetRawSize() const = 0; virtual u64 GetDataSize() const = 0; - virtual bool IsDataSizeAccurate() const = 0; + virtual DataSizeType GetDataSizeType() const = 0; // Returns 0 if the format does not use blocks virtual u64 GetBlockSize() const = 0; diff --git a/Source/Core/DiscIO/CISOBlob.h b/Source/Core/DiscIO/CISOBlob.h index e7dacb3647..ddbd5d0719 100644 --- a/Source/Core/DiscIO/CISOBlob.h +++ b/Source/Core/DiscIO/CISOBlob.h @@ -38,10 +38,8 @@ public: BlobType GetBlobType() const override { return BlobType::CISO; } u64 GetRawSize() const override; - // The CISO format does not save the original file size. - // This function returns an upper bound. u64 GetDataSize() const override; - bool IsDataSizeAccurate() const override { return false; } + DataSizeType GetDataSizeType() const override { return DataSizeType::UpperBound; } u64 GetBlockSize() const override { return m_block_size; } bool HasFastRandomAccessInBlock() const override { return true; } diff --git a/Source/Core/DiscIO/CMakeLists.txt b/Source/Core/DiscIO/CMakeLists.txt index ec1562325a..61790d89d8 100644 --- a/Source/Core/DiscIO/CMakeLists.txt +++ b/Source/Core/DiscIO/CMakeLists.txt @@ -30,6 +30,8 @@ add_library(discio MultithreadedCompressor.h NANDImporter.cpp NANDImporter.h + NFSBlob.cpp + NFSBlob.h RiivolutionParser.cpp RiivolutionParser.h RiivolutionPatcher.cpp diff --git a/Source/Core/DiscIO/CompressedBlob.cpp b/Source/Core/DiscIO/CompressedBlob.cpp index 056c2f4fd3..139a1e6d17 100644 --- a/Source/Core/DiscIO/CompressedBlob.cpp +++ b/Source/Core/DiscIO/CompressedBlob.cpp @@ -273,7 +273,7 @@ bool ConvertToGCZ(BlobReader* infile, const std::string& infile_path, const std::string& outfile_path, u32 sub_type, int block_size, CompressCB callback) { - ASSERT(infile->IsDataSizeAccurate()); + ASSERT(infile->GetDataSizeType() == DataSizeType::Accurate); File::IOFile outfile(outfile_path, "wb"); if (!outfile) diff --git a/Source/Core/DiscIO/CompressedBlob.h b/Source/Core/DiscIO/CompressedBlob.h index e32cc2569d..c828b1ad2b 100644 --- a/Source/Core/DiscIO/CompressedBlob.h +++ b/Source/Core/DiscIO/CompressedBlob.h @@ -53,7 +53,7 @@ public: u64 GetRawSize() const override { return m_file_size; } u64 GetDataSize() const override { return m_header.data_size; } - bool IsDataSizeAccurate() const override { return true; } + DataSizeType GetDataSizeType() const override { return DataSizeType::Accurate; } u64 GetBlockSize() const override { return m_header.block_size; } bool HasFastRandomAccessInBlock() const override { return false; } diff --git a/Source/Core/DiscIO/DirectoryBlob.cpp b/Source/Core/DiscIO/DirectoryBlob.cpp index 7decaac2e3..295235c605 100644 --- a/Source/Core/DiscIO/DirectoryBlob.cpp +++ b/Source/Core/DiscIO/DirectoryBlob.cpp @@ -668,7 +668,7 @@ void DirectoryBlobReader::SetPartitions(std::vector&& partiti m_partitions.emplace(partition_data_offset, std::move(partitions[i].partition)); m_nonpartition_contents.Add(partition_data_offset, data_size, ContentPartition{this, 0, partition_data_offset}); - const u64 unaligned_next_partition_address = VolumeWii::EncryptedPartitionOffsetToRawOffset( + const u64 unaligned_next_partition_address = VolumeWii::OffsetInHashedPartitionToRawOffset( data_size, Partition(partition_address), PARTITION_DATA_OFFSET); partition_address = Common::AlignUp(unaligned_next_partition_address, 0x10000ull); } @@ -743,7 +743,7 @@ void DirectoryBlobReader::SetPartitionHeader(DirectoryBlobPartition* partition, if (wrapped_partition) { - if (m_wrapped_volume->IsEncryptedAndHashed()) + if (m_wrapped_volume->HasWiiHashes()) { const std::optional offset = m_wrapped_volume->ReadSwappedAndShifted( wrapped_partition->offset + WII_PARTITION_H3_OFFSET_ADDRESS, PARTITION_NONE); diff --git a/Source/Core/DiscIO/DirectoryBlob.h b/Source/Core/DiscIO/DirectoryBlob.h index a06e60688f..f616adc876 100644 --- a/Source/Core/DiscIO/DirectoryBlob.h +++ b/Source/Core/DiscIO/DirectoryBlob.h @@ -284,7 +284,7 @@ public: 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 { return 0; } bool HasFastRandomAccessInBlock() const override { return true; } diff --git a/Source/Core/DiscIO/DiscExtractor.cpp b/Source/Core/DiscIO/DiscExtractor.cpp index d0453d7669..229e2a59cc 100644 --- a/Source/Core/DiscIO/DiscExtractor.cpp +++ b/Source/Core/DiscIO/DiscExtractor.cpp @@ -286,7 +286,7 @@ bool ExportSystemData(const Volume& volume, const Partition& partition, success &= ExportTicket(volume, partition, export_folder + "/ticket.bin"); success &= ExportTMD(volume, partition, export_folder + "/tmd.bin"); success &= ExportCertificateChain(volume, partition, export_folder + "/cert.bin"); - if (volume.IsEncryptedAndHashed()) + if (volume.HasWiiHashes()) success &= ExportH3Hashes(volume, partition, export_folder + "/h3.bin"); } diff --git a/Source/Core/DiscIO/DiscScrubber.cpp b/Source/Core/DiscIO/DiscScrubber.cpp index de0f17bb64..22aa06571e 100644 --- a/Source/Core/DiscIO/DiscScrubber.cpp +++ b/Source/Core/DiscIO/DiscScrubber.cpp @@ -30,7 +30,7 @@ bool DiscScrubber::SetupScrub(const Volume* disc) return false; m_disc = disc; - m_file_size = m_disc->GetSize(); + m_file_size = m_disc->GetDataSize(); // Round up when diving by CLUSTER_SIZE, otherwise MarkAsUsed might write out of bounds const size_t num_clusters = static_cast((m_file_size + CLUSTER_SIZE - 1) / CLUSTER_SIZE); @@ -47,7 +47,11 @@ bool DiscScrubber::SetupScrub(const Volume* disc) bool DiscScrubber::CanBlockBeScrubbed(u64 offset) const { - return m_is_scrubbing && m_free_table[offset / CLUSTER_SIZE]; + if (!m_is_scrubbing) + return false; + + const u64 cluster_index = offset / CLUSTER_SIZE; + return cluster_index >= m_free_table.size() || m_free_table[cluster_index]; } void DiscScrubber::MarkAsUsed(u64 offset, u64 size) @@ -92,7 +96,7 @@ void DiscScrubber::MarkAsUsedE(u64 partition_data_offset, u64 offset, u64 size) // Compensate for 0x400 (SHA-1) per 0x8000 (cluster), and round to whole clusters u64 DiscScrubber::ToClusterOffset(u64 offset) const { - if (m_disc->IsEncryptedAndHashed()) + if (m_disc->HasWiiHashes()) return offset / 0x7c00 * CLUSTER_SIZE; else return Common::AlignDown(offset, CLUSTER_SIZE); diff --git a/Source/Core/DiscIO/DriveBlob.h b/Source/Core/DiscIO/DriveBlob.h index afe3dc8953..bea2e827e5 100644 --- a/Source/Core/DiscIO/DriveBlob.h +++ b/Source/Core/DiscIO/DriveBlob.h @@ -27,7 +27,7 @@ public: u64 GetRawSize() const override { return m_size; } u64 GetDataSize() const override { return m_size; } - bool IsDataSizeAccurate() const override { return true; } + DataSizeType GetDataSizeType() const override { return DataSizeType::Accurate; } u64 GetBlockSize() const override { return ECC_BLOCK_SIZE; } bool HasFastRandomAccessInBlock() const override { return false; } diff --git a/Source/Core/DiscIO/FileBlob.cpp b/Source/Core/DiscIO/FileBlob.cpp index e185fe9cc2..6011f45ca9 100644 --- a/Source/Core/DiscIO/FileBlob.cpp +++ b/Source/Core/DiscIO/FileBlob.cpp @@ -44,7 +44,7 @@ bool PlainFileReader::Read(u64 offset, u64 nbytes, u8* out_ptr) bool ConvertToPlain(BlobReader* infile, const std::string& infile_path, const std::string& outfile_path, CompressCB callback) { - ASSERT(infile->IsDataSizeAccurate()); + ASSERT(infile->GetDataSizeType() == DataSizeType::Accurate); File::IOFile outfile(outfile_path, "wb"); if (!outfile) diff --git a/Source/Core/DiscIO/FileBlob.h b/Source/Core/DiscIO/FileBlob.h index ea0bd55b89..d478e1a223 100644 --- a/Source/Core/DiscIO/FileBlob.h +++ b/Source/Core/DiscIO/FileBlob.h @@ -22,7 +22,7 @@ public: u64 GetRawSize() const override { return m_size; } u64 GetDataSize() const override { return m_size; } - 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; } diff --git a/Source/Core/DiscIO/NFSBlob.cpp b/Source/Core/DiscIO/NFSBlob.cpp new file mode 100644 index 0000000000..da558361da --- /dev/null +++ b/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 +#include +#include +#include +#include +#include +#include +#include + +#include + +#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 NFSFileReader::GetLBARanges(const NFSHeader& header) +{ + const size_t lba_range_count = + std::min(Common::swap32(header.lba_range_count), header.lba_ranges.size()); + + std::vector 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 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 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& 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& 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::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 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 lba_ranges = GetLBARanges(header); + + const u64 expected_raw_size = CalculateExpectedRawSize(lba_ranges); + + u64 raw_size; + std::vector files = + OpenFiles(directory, std::move(first_file), expected_raw_size, &raw_size); + + if (files.empty()) + return nullptr; + + return std::unique_ptr( + new NFSFileReader(std::move(lba_ranges), std::move(files), key, raw_size)); +} + +NFSFileReader::NFSFileReader(std::vector lba_ranges, std::vector 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::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 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::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 diff --git a/Source/Core/DiscIO/NFSBlob.h b/Source/Core/DiscIO/NFSBlob.h new file mode 100644 index 0000000000..c17687471d --- /dev/null +++ b/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 +#include +#include +#include +#include + +#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 lba_ranges; + u32 end_magic; // SGGE +}; +static_assert(sizeof(NFSHeader) == 0x200); + +class NFSFileReader : public BlobReader +{ +public: + static std::unique_ptr 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 GetCompressionLevel() const override { return std::nullopt; } + + bool Read(u64 offset, u64 nbytes, u8* out_ptr) override; + +private: + using Key = std::array; + 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 GetLBARanges(const NFSHeader& header); + static std::vector OpenFiles(const std::string& directory, File::IOFile first_file, + u64 expected_raw_size, u64* raw_size_out); + static u64 CalculateExpectedRawSize(const std::vector& lba_ranges); + static u64 CalculateExpectedDataSize(const std::vector& lba_ranges); + + NFSFileReader(std::vector lba_ranges, std::vector 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 m_current_block_encrypted; + std::array m_current_block_decrypted; + u64 m_current_logical_block_index = std::numeric_limits::max(); + + std::vector m_lba_ranges; + std::vector m_files; + std::unique_ptr m_aes_context; + u64 m_raw_size; + u64 m_data_size; +}; + +} // namespace DiscIO diff --git a/Source/Core/DiscIO/ScrubbedBlob.h b/Source/Core/DiscIO/ScrubbedBlob.h index 6530860afe..fa5648630b 100644 --- a/Source/Core/DiscIO/ScrubbedBlob.h +++ b/Source/Core/DiscIO/ScrubbedBlob.h @@ -22,7 +22,7 @@ public: 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 diff --git a/Source/Core/DiscIO/TGCBlob.h b/Source/Core/DiscIO/TGCBlob.h index b6d393ea7d..2db1c9e8b7 100644 --- a/Source/Core/DiscIO/TGCBlob.h +++ b/Source/Core/DiscIO/TGCBlob.h @@ -45,7 +45,7 @@ public: 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; } diff --git a/Source/Core/DiscIO/Volume.h b/Source/Core/DiscIO/Volume.h index 2c974f447d..3747c4c250 100644 --- a/Source/Core/DiscIO/Volume.h +++ b/Source/Core/DiscIO/Volume.h @@ -22,6 +22,7 @@ namespace DiscIO { class BlobReader; enum class BlobType; +enum class DataSizeType; class FileSystem; class VolumeDisc; class VolumeWAD; @@ -63,7 +64,8 @@ public: return static_cast(*temp) << GetOffsetShift(); } - virtual bool IsEncryptedAndHashed() const { return false; } + virtual bool HasWiiHashes() const { return false; } + virtual bool HasWiiEncryption() const { return false; } virtual std::vector GetPartitions() const { return {}; } virtual Partition GetGamePartition() const { return PARTITION_NONE; } virtual std::optional GetPartitionType(const Partition& partition) const @@ -122,7 +124,6 @@ public: 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 @@ -137,8 +138,8 @@ public: 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; diff --git a/Source/Core/DiscIO/VolumeFileBlobReader.h b/Source/Core/DiscIO/VolumeFileBlobReader.h index 88b0929cbc..89b87a9470 100644 --- a/Source/Core/DiscIO/VolumeFileBlobReader.h +++ b/Source/Core/DiscIO/VolumeFileBlobReader.h @@ -25,7 +25,7 @@ public: 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; diff --git a/Source/Core/DiscIO/VolumeGC.cpp b/Source/Core/DiscIO/VolumeGC.cpp index 8279b84820..1517207f95 100644 --- a/Source/Core/DiscIO/VolumeGC.cpp +++ b/Source/Core/DiscIO/VolumeGC.cpp @@ -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 diff --git a/Source/Core/DiscIO/VolumeGC.h b/Source/Core/DiscIO/VolumeGC.h index 4dfe64cac4..a87ccd44d8 100644 --- a/Source/Core/DiscIO/VolumeGC.h +++ b/Source/Core/DiscIO/VolumeGC.h @@ -45,8 +45,8 @@ public: 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; diff --git a/Source/Core/DiscIO/VolumeVerifier.cpp b/Source/Core/DiscIO/VolumeVerifier.cpp index c4c5539fe2..50cfb2a17f 100644 --- a/Source/Core/DiscIO/VolumeVerifier.cpp +++ b/Source/Core/DiscIO/VolumeVerifier.cpp @@ -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(); @@ -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; @@ -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 partitions = CheckPartitions(); @@ -492,7 +491,7 @@ std::vector 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) { @@ -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); @@ -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) { @@ -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; @@ -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; @@ -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{}; @@ -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; @@ -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 { @@ -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 += diff --git a/Source/Core/DiscIO/VolumeVerifier.h b/Source/Core/DiscIO/VolumeVerifier.h index abd8c7f8ee..934e60b7b1 100644 --- a/Source/Core/DiscIO/VolumeVerifier.h +++ b/Source/Core/DiscIO/VolumeVerifier.h @@ -202,6 +202,7 @@ private: bool m_done = false; u64 m_progress = 0; u64 m_max_progress = 0; + DataSizeType m_data_size_type; }; } // namespace DiscIO diff --git a/Source/Core/DiscIO/VolumeWad.cpp b/Source/Core/DiscIO/VolumeWad.cpp index edfd4c76ee..81e2912671 100644 --- a/Source/Core/DiscIO/VolumeWad.cpp +++ b/Source/Core/DiscIO/VolumeWad.cpp @@ -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 diff --git a/Source/Core/DiscIO/VolumeWad.h b/Source/Core/DiscIO/VolumeWad.h index ac18b65271..3b723b7ce9 100644 --- a/Source/Core/DiscIO/VolumeWad.h +++ b/Source/Core/DiscIO/VolumeWad.h @@ -64,8 +64,8 @@ public: 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; diff --git a/Source/Core/DiscIO/VolumeWii.cpp b/Source/Core/DiscIO/VolumeWii.cpp index da7254e845..eca23fb4f9 100644 --- a/Source/Core/DiscIO/VolumeWii.cpp +++ b/Source/Core/DiscIO/VolumeWii.cpp @@ -41,7 +41,11 @@ VolumeWii::VolumeWii(std::unique_ptr reader) { ASSERT(m_reader); - m_encrypted = m_reader->ReadSwapped(0x60) == u32(0); + m_has_hashes = m_reader->ReadSwapped(0x60) == u8(0); + m_has_encryption = m_reader->ReadSwapped(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) { @@ -114,7 +118,7 @@ VolumeWii::VolumeWii(std::unique_ptr reader) }; auto get_h3_table = [this, partition]() -> std::vector { - if (!m_encrypted) + if (!m_has_hashes) return {}; const std::optional h3_table_offset = ReadSwappedAndShifted( partition.offset + WII_PARTITION_H3_OFFSET_ADDRESS, PARTITION_NONE); @@ -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)) - return m_reader->ReadWiiDecrypted(offset, length, buffer, partition_data_offset); - - if (!m_encrypted) + if (m_has_hashes && m_has_encryption && + m_reader->SupportsReadWiiDecrypted(offset, length, partition_data_offset)) { - return m_reader->Read(partition.offset + *partition_details.data_offset + offset, length, - buffer); + return m_reader->ReadWiiDecrypted(offset, length, buffer, partition_data_offset); } - auto aes_context = partition_details.key->get(); - if (!aes_context) - return false; + if (!m_has_hashes) + { + return m_reader->Read(partition_data_offset + offset, length, buffer); + } + + Common::AES::Context* aes_context = nullptr; + std::unique_ptr read_buffer = nullptr; + if (m_has_encryption) + { + aes_context = partition_details.key->get(); + if (!aes_context) + return false; + + read_buffer = std::make_unique(BLOCK_TOTAL_SIZE); + } - auto read_buffer = std::make_unique(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; } @@ -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 VolumeWii::GetPartitions() const @@ -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; @@ -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 @@ -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 @@ -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(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]) diff --git a/Source/Core/DiscIO/VolumeWii.h b/Source/Core/DiscIO/VolumeWii.h index d4837a26b6..b1ed4b9789 100644 --- a/Source/Core/DiscIO/VolumeWii.h +++ b/Source/Core/DiscIO/VolumeWii.h @@ -60,7 +60,8 @@ public: VolumeWii(std::unique_ptr 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 GetPartitions() const override; Partition GetGamePartition() const override; std::optional GetPartitionType(const Partition& partition) const override; @@ -69,8 +70,8 @@ public: const IOS::ES::TMDReader& GetTMD(const Partition& partition) const override; const std::vector& 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 GetLongNames() const override; @@ -78,7 +79,6 @@ public: 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; @@ -86,8 +86,8 @@ public: 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 GetSyncHash() const override; @@ -128,7 +128,8 @@ private: std::unique_ptr m_reader; std::map 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]{}; diff --git a/Source/Core/DiscIO/WIABlob.cpp b/Source/Core/DiscIO/WIABlob.cpp index 35b6407d66..b80753120b 100644 --- a/Source/Core/DiscIO/WIABlob.cpp +++ b/Source/Core/DiscIO/WIABlob.cpp @@ -925,7 +925,7 @@ ConversionResultCode WIARVZFileReader::SetUpDataEntriesForWriting( std::vector* data_entries, std::vector* partition_file_systems) { std::vector partitions; - if (volume && volume->IsEncryptedAndHashed()) + if (volume && volume->HasWiiHashes() && volume->HasWiiEncryption()) partitions = volume->GetPartitions(); std::sort(partitions.begin(), partitions.end(), @@ -1731,7 +1731,7 @@ WIARVZFileReader::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(); diff --git a/Source/Core/DiscIO/WIABlob.h b/Source/Core/DiscIO/WIABlob.h index d998005d06..633c5801ce 100644 --- a/Source/Core/DiscIO/WIABlob.h +++ b/Source/Core/DiscIO/WIABlob.h @@ -52,7 +52,7 @@ public: 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; } diff --git a/Source/Core/DiscIO/WbfsBlob.h b/Source/Core/DiscIO/WbfsBlob.h index 30db2a7d4c..a981790f1c 100644 --- a/Source/Core/DiscIO/WbfsBlob.h +++ b/Source/Core/DiscIO/WbfsBlob.h @@ -25,11 +25,8 @@ public: 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; } diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props index 0c73e3d980..592b423164 100644 --- a/Source/Core/DolphinLib.props +++ b/Source/Core/DolphinLib.props @@ -442,6 +442,7 @@ + @@ -1056,6 +1057,7 @@ + diff --git a/Source/Core/DolphinQt/GameList/GameTracker.cpp b/Source/Core/DolphinQt/GameList/GameTracker.cpp index b783cc0ab3..59421db9e8 100644 --- a/Source/Core/DolphinQt/GameList/GameTracker.cpp +++ b/Source/Core/DolphinQt/GameList/GameTracker.cpp @@ -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) { diff --git a/Source/Core/DolphinQt/Info.plist.in b/Source/Core/DolphinQt/Info.plist.in index b5f3a3f44b..2f5a108cb3 100644 --- a/Source/Core/DolphinQt/Info.plist.in +++ b/Source/Core/DolphinQt/Info.plist.in @@ -14,6 +14,7 @@ gcz iso m3u + nfs rvz tgc wad diff --git a/Source/Core/DolphinQt/MainWindow.cpp b/Source/Core/DolphinQt/MainWindow.cpp index 281bbc78a8..7c4e0a0dd7 100644 --- a/Source/Core/DolphinQt/MainWindow.cpp +++ b/Source/Core/DolphinQt/MainWindow.cpp @@ -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"))); diff --git a/Source/Core/DolphinQt/Settings/PathPane.cpp b/Source/Core/DolphinQt/Settings/PathPane.cpp index 6ccc1c2a3b..3382f8f7f9 100644 --- a/Source/Core/DolphinQt/Settings/PathPane.cpp +++ b/Source/Core/DolphinQt/Settings/PathPane.cpp @@ -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")))); diff --git a/Source/Core/DolphinTool/ConvertCommand.cpp b/Source/Core/DolphinTool/ConvertCommand.cpp index 4d5e2062fa..94b7f7fd11 100644 --- a/Source/Core/DolphinTool/ConvertCommand.cpp +++ b/Source/Core/DolphinTool/ConvertCommand.cpp @@ -212,7 +212,7 @@ int ConvertCommand::Main(const std::vector& 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 " diff --git a/Source/Core/UICommon/GameFile.cpp b/Source/Core/UICommon/GameFile.cpp index e605f987eb..7ab763b785 100644 --- a/Source/Core/UICommon/GameFile.cpp +++ b/Source/Core/UICommon/GameFile.cpp @@ -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(); @@ -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; @@ -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); @@ -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 diff --git a/Source/Core/UICommon/GameFile.h b/Source/Core/UICommon/GameFile.h index eb1151a28a..fca89073a1 100644 --- a/Source/Core/UICommon/GameFile.h +++ b/Source/Core/UICommon/GameFile.h @@ -104,7 +104,7 @@ public: 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; @@ -145,7 +145,7 @@ private: 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{}; diff --git a/Source/Core/UICommon/GameFileCache.cpp b/Source/Core/UICommon/GameFileCache.cpp index dbb349dad9..f9b26d344d 100644 --- a/Source/Core/UICommon/GameFileCache.cpp +++ b/Source/Core/UICommon/GameFileCache.cpp @@ -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 FindAllGamePaths(const std::vector& directories_to_scan, bool recursive_scan) { - static const std::vector search_extensions = {".gcm", ".tgc", ".iso", ".ciso", - ".gcz", ".wbfs", ".wia", ".rvz", - ".wad", ".dol", ".elf", ".json"}; + static const std::vector 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);