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/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..d6a81d7c2d 100644 --- a/Source/Core/DiscIO/Blob.h +++ b/Source/Core/DiscIO/Blob.h @@ -40,6 +40,7 @@ enum class BlobType WIA, RVZ, MOD_DESCRIPTOR, + NFS, }; std::string GetName(BlobType blob_type, bool translate); 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/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..847c66324a --- /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; + bool IsDataSizeAccurate() const override { return false; } + + 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/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/UICommon/GameFileCache.cpp b/Source/Core/UICommon/GameFileCache.cpp index dbb349dad9..675ec0dc43 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 = 22; // 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);