mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-02-13 07:49:19 +01:00
Merge pull request #10932 from JosJuice/nfs
DiscIO: Add support for the NFS format
This commit is contained in:
commit
5508c52a95
@ -30,7 +30,8 @@ import java.util.Set;
|
||||
public final class FileBrowserHelper
|
||||
{
|
||||
public static final HashSet<String> 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<String> GAME_LIKE_EXTENSIONS = new HashSet<>(GAME_EXTENSIONS);
|
||||
|
||||
|
@ -231,7 +231,7 @@ std::unique_ptr<BootParameters> BootParameters::GenerateFromFile(std::vector<std
|
||||
#endif
|
||||
|
||||
static const std::unordered_set<std::string> 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<DiscIO::VolumeDisc> disc = DiscIO::CreateDisc(path);
|
||||
|
@ -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<DiscIO::VolumeDisc> 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
|
||||
{
|
||||
|
@ -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()
|
||||
|
@ -41,7 +41,7 @@ void DoState(PointerWrap& p);
|
||||
void SetDisc(std::unique_ptr<DiscIO::Volume> 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);
|
||||
|
@ -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<BlobReader> 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);
|
||||
|
@ -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;
|
||||
|
@ -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; }
|
||||
|
@ -30,6 +30,8 @@ add_library(discio
|
||||
MultithreadedCompressor.h
|
||||
NANDImporter.cpp
|
||||
NANDImporter.h
|
||||
NFSBlob.cpp
|
||||
NFSBlob.h
|
||||
RiivolutionParser.cpp
|
||||
RiivolutionParser.h
|
||||
RiivolutionPatcher.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)
|
||||
|
@ -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; }
|
||||
|
@ -668,7 +668,7 @@ void DirectoryBlobReader::SetPartitions(std::vector<PartitionWithType>&& 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<u64> offset = m_wrapped_volume->ReadSwappedAndShifted(
|
||||
wrapped_partition->offset + WII_PARTITION_H3_OFFSET_ADDRESS, PARTITION_NONE);
|
||||
|
@ -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; }
|
||||
|
@ -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");
|
||||
}
|
||||
|
||||
|
@ -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<size_t>((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);
|
||||
|
@ -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; }
|
||||
|
@ -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)
|
||||
|
@ -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; }
|
||||
|
306
Source/Core/DiscIO/NFSBlob.cpp
Normal file
306
Source/Core/DiscIO/NFSBlob.cpp
Normal file
@ -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
Source/Core/DiscIO/NFSBlob.h
Normal file
91
Source/Core/DiscIO/NFSBlob.h
Normal file
@ -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
|
@ -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
|
||||
|
@ -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; }
|
||||
|
@ -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<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
|
||||
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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<Partition> partitions = CheckPartitions();
|
||||
|
||||
@ -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)
|
||||
{
|
||||
@ -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 +=
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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)
|
||||
{
|
||||
@ -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);
|
||||
@ -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<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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
@ -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<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])
|
||||
|
@ -60,7 +60,8 @@ public:
|
||||
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;
|
||||
@ -69,8 +70,8 @@ public:
|
||||
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;
|
||||
@ -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<u8, 20> GetSyncHash() const override;
|
||||
@ -128,7 +128,8 @@ private:
|
||||
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]{};
|
||||
|
@ -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(),
|
||||
@ -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();
|
||||
|
@ -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; }
|
||||
|
@ -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; }
|
||||
|
@ -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" />
|
||||
@ -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" />
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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>
|
||||
|
@ -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")));
|
||||
|
||||
|
@ -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"))));
|
||||
|
||||
|
@ -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 "
|
||||
|
@ -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
|
||||
|
@ -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{};
|
||||
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user