diff --git a/Source/Core/DiscIO/Blob.h b/Source/Core/DiscIO/Blob.h index 52b083ab63..1d23d7b6b0 100644 --- a/Source/Core/DiscIO/Blob.h +++ b/Source/Core/DiscIO/Blob.h @@ -41,11 +41,16 @@ class BlobReader { public: virtual ~BlobReader() {} + virtual BlobType GetBlobType() const = 0; + virtual u64 GetRawSize() const = 0; virtual u64 GetDataSize() const = 0; virtual bool IsDataSizeAccurate() const = 0; + // Returns 0 if the format does not use blocks + virtual u64 GetBlockSize() const { return 0; } + // NOT thread-safe - can't call this from multiple threads. virtual bool Read(u64 offset, u64 size, u8* out_ptr) = 0; template @@ -160,10 +165,11 @@ std::unique_ptr CreateBlobReader(const std::string& filename); typedef bool (*CompressCB)(const std::string& text, float percent, void* arg); -bool CompressFileToBlob(const std::string& infile_path, const std::string& outfile_path, - u32 sub_type = 0, int sector_size = 16384, CompressCB callback = nullptr, - void* arg = nullptr); -bool DecompressBlobToFile(const std::string& infile_path, const std::string& outfile_path, - CompressCB callback = nullptr, void* arg = nullptr); +bool ConvertToGCZ(BlobReader* infile, const std::string& infile_path, + const std::string& outfile_path, u32 sub_type, int sector_size = 16384, + CompressCB callback = nullptr, void* arg = nullptr); +bool ConvertToPlain(BlobReader* infile, const std::string& infile_path, + const std::string& outfile_path, CompressCB callback = nullptr, + void* arg = nullptr); } // namespace DiscIO diff --git a/Source/Core/DiscIO/CISOBlob.h b/Source/Core/DiscIO/CISOBlob.h index dc606bb4a5..66f950f074 100644 --- a/Source/Core/DiscIO/CISOBlob.h +++ b/Source/Core/DiscIO/CISOBlob.h @@ -37,12 +37,15 @@ public: static std::unique_ptr Create(File::IOFile file); 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; } - u64 GetRawSize() const override; + u64 GetBlockSize() const override { return m_block_size; } + bool Read(u64 offset, u64 nbytes, u8* out_ptr) override; private: diff --git a/Source/Core/DiscIO/CMakeLists.txt b/Source/Core/DiscIO/CMakeLists.txt index 279e72030d..4970f135c2 100644 --- a/Source/Core/DiscIO/CMakeLists.txt +++ b/Source/Core/DiscIO/CMakeLists.txt @@ -23,6 +23,8 @@ add_library(discio Filesystem.h NANDImporter.cpp NANDImporter.h + ScrubbedBlob.cpp + ScrubbedBlob.h TGCBlob.cpp TGCBlob.h Volume.cpp diff --git a/Source/Core/DiscIO/CompressedBlob.cpp b/Source/Core/DiscIO/CompressedBlob.cpp index 5248170683..cda3cad6d3 100644 --- a/Source/Core/DiscIO/CompressedBlob.cpp +++ b/Source/Core/DiscIO/CompressedBlob.cpp @@ -17,6 +17,7 @@ #include #include +#include "Common/Assert.h" #include "Common/CommonTypes.h" #include "Common/File.h" #include "Common/FileUtil.h" @@ -153,23 +154,11 @@ bool CompressedBlobReader::GetBlock(u64 block_num, u8* out_ptr) return true; } -bool CompressFileToBlob(const std::string& infile_path, const std::string& outfile_path, - u32 sub_type, int block_size, CompressCB callback, void* arg) +bool ConvertToGCZ(BlobReader* infile, const std::string& infile_path, + const std::string& outfile_path, u32 sub_type, int block_size, + CompressCB callback, void* arg) { - bool scrubbing = false; - - File::IOFile infile(infile_path, "rb"); - if (IsGCZBlob(infile)) - { - PanicAlertT("\"%s\" is already compressed! Cannot compress it further.", infile_path.c_str()); - return false; - } - - if (!infile) - { - PanicAlertT("Failed to open the input file \"%s\".", infile_path.c_str()); - return false; - } + ASSERT(infile->IsDataSizeAccurate()); File::IOFile outfile(outfile_path, "wb"); if (!outfile) @@ -181,21 +170,6 @@ bool CompressFileToBlob(const std::string& infile_path, const std::string& outfi return false; } - DiscScrubber disc_scrubber; - std::unique_ptr volume; - if (sub_type == 1) - { - volume = CreateDisc(infile_path); - if (!volume || !disc_scrubber.SetupScrub(volume.get(), block_size)) - { - PanicAlertT("\"%s\" failed to be scrubbed. Probably the image is corrupt.", - infile_path.c_str()); - return false; - } - - scrubbing = true; - } - z_stream z = {}; if (deflateInit(&z, 9) != Z_OK) return false; @@ -206,7 +180,7 @@ bool CompressFileToBlob(const std::string& infile_path, const std::string& outfi header.magic_cookie = GCZ_MAGIC; header.sub_type = sub_type; header.block_size = block_size; - header.data_size = infile.GetSize(); + header.data_size = infile->GetDataSize(); // round upwards! header.num_blocks = (u32)((header.data_size + (block_size - 1)) / block_size); @@ -220,10 +194,9 @@ bool CompressFileToBlob(const std::string& infile_path, const std::string& outfi outfile.Seek(sizeof(CompressedBlobHeader), SEEK_CUR); // seek past the offset and hash tables (we will write them at the end) outfile.Seek((sizeof(u64) + sizeof(u32)) * header.num_blocks, SEEK_CUR); - // seek to the start of the input file to make sure we get everything - infile.Seek(0, SEEK_SET); // Now we are ready to write compressed data! + u64 inpos = 0; u64 position = 0; int num_compressed = 0; int num_stored = 0; @@ -234,7 +207,6 @@ bool CompressFileToBlob(const std::string& infile_path, const std::string& outfi { if (i % progress_monitor == 0) { - const u64 inpos = infile.Tell(); int ratio = 0; if (inpos != 0) ratio = (int)(100 * position / inpos); @@ -252,13 +224,16 @@ bool CompressFileToBlob(const std::string& infile_path, const std::string& outfi offsets[i] = position; - size_t read_bytes; - if (scrubbing) - read_bytes = disc_scrubber.GetNextBlock(infile, in_buf.data()); - else - infile.ReadArray(in_buf.data(), header.block_size, &read_bytes); - if (read_bytes < header.block_size) - std::fill(in_buf.begin() + read_bytes, in_buf.begin() + header.block_size, 0); + const u64 bytes_to_read = std::min(block_size, header.data_size - inpos); + + success = infile->Read(inpos, bytes_to_read, in_buf.data()); + if (!success) + { + PanicAlertT("Failed to read from the input file \"%s\".", infile_path.c_str()); + break; + } + + std::fill(in_buf.begin() + bytes_to_read, in_buf.begin() + header.block_size, 0); int retval = deflateReset(&z); z.next_in = in_buf.data(); @@ -305,6 +280,7 @@ bool CompressFileToBlob(const std::string& infile_path, const std::string& outfi break; } + inpos += block_size; position += write_size; hashes[i] = Common::HashAdler32(write_buf, write_size); @@ -337,84 +313,6 @@ bool CompressFileToBlob(const std::string& infile_path, const std::string& outfi return success; } -bool DecompressBlobToFile(const std::string& infile_path, const std::string& outfile_path, - CompressCB callback, void* arg) -{ - std::unique_ptr reader; - { - File::IOFile infile(infile_path, "rb"); - if (!IsGCZBlob(infile)) - { - PanicAlertT("File not compressed"); - return false; - } - - reader = CompressedBlobReader::Create(std::move(infile), infile_path); - } - - if (!reader) - { - PanicAlertT("Failed to open the input file \"%s\".", infile_path.c_str()); - return false; - } - - File::IOFile outfile(outfile_path, "wb"); - if (!outfile) - { - PanicAlertT("Failed to open the output file \"%s\".\n" - "Check that you have permissions to write the target folder and that the media can " - "be written.", - outfile_path.c_str()); - return false; - } - - const CompressedBlobHeader& header = reader->GetHeader(); - static const size_t BUFFER_BLOCKS = 32; - size_t buffer_size = header.block_size * BUFFER_BLOCKS; - std::vector buffer(buffer_size); - u32 num_buffers = (header.num_blocks + BUFFER_BLOCKS - 1) / BUFFER_BLOCKS; - int progress_monitor = std::max(1, num_buffers / 100); - bool success = true; - - for (u64 i = 0; i < num_buffers; i++) - { - if (i % progress_monitor == 0) - { - const bool was_cancelled = - !callback(Common::GetStringT("Unpacking"), (float)i / (float)num_buffers, arg); - if (was_cancelled) - { - success = false; - break; - } - } - const u64 inpos = i * buffer_size; - const u64 sz = std::min(buffer_size, header.data_size - inpos); - reader->Read(inpos, sz, buffer.data()); - if (!outfile.WriteBytes(buffer.data(), sz)) - { - PanicAlertT("Failed to write the output file \"%s\".\n" - "Check that you have enough space available on the target drive.", - outfile_path.c_str()); - success = false; - break; - } - } - - if (!success) - { - // Remove the incomplete output file. - outfile.Close(); - File::Delete(outfile_path); - } - else - { - outfile.Resize(header.data_size); - } - - return success; -} - bool IsGCZBlob(File::IOFile& file) { const u64 position = file.Tell(); diff --git a/Source/Core/DiscIO/CompressedBlob.h b/Source/Core/DiscIO/CompressedBlob.h index 0e290542c6..8c63aa68d6 100644 --- a/Source/Core/DiscIO/CompressedBlob.h +++ b/Source/Core/DiscIO/CompressedBlob.h @@ -52,6 +52,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; } + u64 GetBlockSize() const override { return m_header.block_size; } u64 GetBlockCompressedSize(u64 block_num) const; bool GetBlock(u64 block_num, u8* out_ptr) override; diff --git a/Source/Core/DiscIO/DiscIO.vcxproj b/Source/Core/DiscIO/DiscIO.vcxproj index dd322dbb62..98bb500452 100644 --- a/Source/Core/DiscIO/DiscIO.vcxproj +++ b/Source/Core/DiscIO/DiscIO.vcxproj @@ -56,6 +56,7 @@ + @@ -80,6 +81,7 @@ + diff --git a/Source/Core/DiscIO/DiscIO.vcxproj.filters b/Source/Core/DiscIO/DiscIO.vcxproj.filters index 5fb5891cfe..9b19fa8471 100644 --- a/Source/Core/DiscIO/DiscIO.vcxproj.filters +++ b/Source/Core/DiscIO/DiscIO.vcxproj.filters @@ -87,6 +87,9 @@ Volume\Blob + + Volume\Blob + @@ -155,6 +158,9 @@ Volume\Blob + + Volume\Blob + diff --git a/Source/Core/DiscIO/DiscScrubber.cpp b/Source/Core/DiscIO/DiscScrubber.cpp index 8a06d9266e..e7b098b374 100644 --- a/Source/Core/DiscIO/DiscScrubber.cpp +++ b/Source/Core/DiscIO/DiscScrubber.cpp @@ -14,6 +14,7 @@ #include #include "Common/Align.h" +#include "Common/Assert.h" #include "Common/CommonTypes.h" #include "Common/File.h" #include "Common/Logging/Log.h" @@ -24,24 +25,14 @@ namespace DiscIO { -constexpr size_t CLUSTER_SIZE = 0x8000; - DiscScrubber::DiscScrubber() = default; DiscScrubber::~DiscScrubber() = default; -bool DiscScrubber::SetupScrub(const Volume* disc, int block_size) +bool DiscScrubber::SetupScrub(const Volume* disc) { if (!disc) return false; m_disc = disc; - m_block_size = block_size; - - if (CLUSTER_SIZE % m_block_size != 0) - { - ERROR_LOG(DISCIO, "Block size %u is not a factor of 0x8000, scrubbing not possible", - m_block_size); - return false; - } m_file_size = m_disc->GetSize(); @@ -54,34 +45,10 @@ bool DiscScrubber::SetupScrub(const Volume* disc, int block_size) // Fill out table of free blocks const bool success = ParseDisc(); - m_block_count = 0; - m_is_scrubbing = success; return success; } -size_t DiscScrubber::GetNextBlock(File::IOFile& in, u8* buffer) -{ - const u64 current_offset = m_block_count * m_block_size; - - size_t read_bytes = 0; - if (CanBlockBeScrubbed(current_offset)) - { - DEBUG_LOG(DISCIO, "Freeing 0x%016" PRIx64, current_offset); - std::fill(buffer, buffer + m_block_size, 0x00); - in.Seek(m_block_size, SEEK_CUR); - read_bytes = m_block_size; - } - else - { - DEBUG_LOG(DISCIO, "Used 0x%016" PRIx64, current_offset); - in.ReadArray(buffer, m_block_size, &read_bytes); - } - - m_block_count++; - return read_bytes; -} - bool DiscScrubber::CanBlockBeScrubbed(u64 offset) const { return m_is_scrubbing && m_free_table[offset / CLUSTER_SIZE]; @@ -89,8 +56,8 @@ bool DiscScrubber::CanBlockBeScrubbed(u64 offset) const void DiscScrubber::MarkAsUsed(u64 offset, u64 size) { - u64 current_offset = offset; - const u64 end_offset = current_offset + size; + u64 current_offset = Common::AlignDown(offset, CLUSTER_SIZE); + const u64 end_offset = offset + size; DEBUG_LOG(DISCIO, "Marking 0x%016" PRIx64 " - 0x%016" PRIx64 " as used", offset, end_offset); @@ -103,20 +70,27 @@ void DiscScrubber::MarkAsUsed(u64 offset, u64 size) void DiscScrubber::MarkAsUsedE(u64 partition_data_offset, u64 offset, u64 size) { - u64 first_cluster_start = ToClusterOffset(offset) + partition_data_offset; - - u64 last_cluster_end; - if (size == 0) + if (partition_data_offset == 0) { - // Without this special case, a size of 0 can be rounded to 1 cluster instead of 0 - last_cluster_end = first_cluster_start; + MarkAsUsed(offset, size); } else { - last_cluster_end = ToClusterOffset(offset + size - 1) + CLUSTER_SIZE + partition_data_offset; - } + u64 first_cluster_start = ToClusterOffset(offset) + partition_data_offset; - MarkAsUsed(first_cluster_start, last_cluster_end - first_cluster_start); + u64 last_cluster_end; + if (size == 0) + { + // Without this special case, a size of 0 can be rounded to 1 cluster instead of 0 + last_cluster_end = first_cluster_start; + } + else + { + last_cluster_end = ToClusterOffset(offset + size - 1) + CLUSTER_SIZE + partition_data_offset; + } + + MarkAsUsed(first_cluster_start, last_cluster_end - first_cluster_start); + } } // Compensate for 0x400 (SHA-1) per 0x8000 (cluster), and round to whole clusters @@ -147,35 +121,38 @@ bool DiscScrubber::ReadFromVolume(u64 offset, u64& buffer, const Partition& part bool DiscScrubber::ParseDisc() { + if (m_disc->GetPartitions().empty()) + return ParsePartitionData(PARTITION_NONE); + // Mark the header as used - it's mostly 0s anyways MarkAsUsed(0, 0x50000); for (const DiscIO::Partition& partition : m_disc->GetPartitions()) { - PartitionHeader header; + u32 tmd_size; + u64 tmd_offset; + u32 cert_chain_size; + u64 cert_chain_offset; + u64 h3_offset; + // The H3 size is always 0x18000 - if (!ReadFromVolume(partition.offset + 0x2a4, header.tmd_size, PARTITION_NONE) || - !ReadFromVolume(partition.offset + 0x2a8, header.tmd_offset, PARTITION_NONE) || - !ReadFromVolume(partition.offset + 0x2ac, header.cert_chain_size, PARTITION_NONE) || - !ReadFromVolume(partition.offset + 0x2b0, header.cert_chain_offset, PARTITION_NONE) || - !ReadFromVolume(partition.offset + 0x2b4, header.h3_offset, PARTITION_NONE) || - !ReadFromVolume(partition.offset + 0x2b8, header.data_offset, PARTITION_NONE) || - !ReadFromVolume(partition.offset + 0x2bc, header.data_size, PARTITION_NONE)) + if (!ReadFromVolume(partition.offset + 0x2a4, tmd_size, PARTITION_NONE) || + !ReadFromVolume(partition.offset + 0x2a8, tmd_offset, PARTITION_NONE) || + !ReadFromVolume(partition.offset + 0x2ac, cert_chain_size, PARTITION_NONE) || + !ReadFromVolume(partition.offset + 0x2b0, cert_chain_offset, PARTITION_NONE) || + !ReadFromVolume(partition.offset + 0x2b4, h3_offset, PARTITION_NONE)) { return false; } MarkAsUsed(partition.offset, 0x2c0); - MarkAsUsed(partition.offset + header.tmd_offset, header.tmd_size); - MarkAsUsed(partition.offset + header.cert_chain_offset, header.cert_chain_size); - MarkAsUsed(partition.offset + header.h3_offset, 0x18000); - // This would mark the whole (encrypted) data area - // we need to parse FST and other crap to find what's free within it! - // MarkAsUsed(partition.offset + header.data_offset, header.data_size); + MarkAsUsed(partition.offset + tmd_offset, tmd_size); + MarkAsUsed(partition.offset + cert_chain_offset, cert_chain_size); + MarkAsUsed(partition.offset + h3_offset, 0x18000); // Parse Data! This is where the big gain is - if (!ParsePartitionData(partition, &header)) + if (!ParsePartitionData(partition)) return false; } @@ -183,7 +160,7 @@ bool DiscScrubber::ParseDisc() } // Operations dealing with encrypted space are done here -bool DiscScrubber::ParsePartitionData(const Partition& partition, PartitionHeader* header) +bool DiscScrubber::ParsePartitionData(const Partition& partition) { const FileSystem* filesystem = m_disc->GetFileSystem(partition); if (!filesystem) @@ -193,17 +170,30 @@ bool DiscScrubber::ParsePartitionData(const Partition& partition, PartitionHeade return false; } - const u64 partition_data_offset = partition.offset + header->data_offset; + u64 partition_data_offset; + if (partition == PARTITION_NONE) + { + partition_data_offset = 0; + } + else + { + u64 data_offset; + if (!ReadFromVolume(partition.offset + 0x2b8, data_offset, PARTITION_NONE)) + return false; + + partition_data_offset = partition.offset + data_offset; + } // Mark things as used which are not in the filesystem // Header, Header Information, Apploader - if (!ReadFromVolume(0x2440 + 0x14, header->apploader_size, partition) || - !ReadFromVolume(0x2440 + 0x18, header->apploader_size, partition)) + u32 apploader_size; + u32 apploader_trailer_size; + if (!ReadFromVolume(0x2440 + 0x14, apploader_size, partition) || + !ReadFromVolume(0x2440 + 0x18, apploader_trailer_size, partition)) { return false; } - MarkAsUsedE(partition_data_offset, 0, - 0x2440 + header->apploader_size + header->apploader_trailer_size); + MarkAsUsedE(partition_data_offset, 0, 0x2440 + apploader_size + apploader_trailer_size); // DOL const std::optional dol_offset = GetBootDOLOffset(*m_disc, partition); @@ -212,17 +202,14 @@ bool DiscScrubber::ParsePartitionData(const Partition& partition, PartitionHeade const std::optional dol_size = GetBootDOLSize(*m_disc, partition, *dol_offset); if (!dol_size) return false; - header->dol_offset = *dol_offset; - header->dol_size = *dol_size; - MarkAsUsedE(partition_data_offset, header->dol_offset, header->dol_size); + MarkAsUsedE(partition_data_offset, *dol_offset, *dol_size); // FST - if (!ReadFromVolume(0x424, header->fst_offset, partition) || - !ReadFromVolume(0x428, header->fst_size, partition)) - { + const std::optional fst_offset = GetFSTOffset(*m_disc, partition); + const std::optional fst_size = GetFSTSize(*m_disc, partition); + if (!fst_offset || !fst_size) return false; - } - MarkAsUsedE(partition_data_offset, header->fst_offset, header->fst_size); + MarkAsUsedE(partition_data_offset, *fst_offset, *fst_size); // Go through the filesystem and mark entries as used ParseFileSystemData(partition_data_offset, filesystem->GetRoot()); diff --git a/Source/Core/DiscIO/DiscScrubber.h b/Source/Core/DiscIO/DiscScrubber.h index 15e0cda7f7..5d64fa5376 100644 --- a/Source/Core/DiscIO/DiscScrubber.h +++ b/Source/Core/DiscIO/DiscScrubber.h @@ -2,11 +2,7 @@ // Licensed under GPLv2+ // Refer to the license.txt file included. -// DiscScrubber removes the garbage data from discs (currently Wii only) which -// is on the disc due to encryption - -// It could be adapted to GameCube discs, but the gain is most likely negligible, -// and having 1:1 backups of discs is always nice when they are reasonably sized +// DiscScrubber removes the pseudorandom padding data from discs // Note: the technique is inspired by Wiiscrubber, but much simpler - intentionally :) @@ -34,46 +30,27 @@ public: DiscScrubber(); ~DiscScrubber(); - bool SetupScrub(const Volume* disc, int block_size); - size_t GetNextBlock(File::IOFile& in, u8* buffer); + bool SetupScrub(const Volume* disc); + + // Returns true if the specified 32 KiB block only contains unused data bool CanBlockBeScrubbed(u64 offset) const; -private: - struct PartitionHeader final - { - u8* ticket[0x2a4]; - u32 tmd_size; - u64 tmd_offset; - u32 cert_chain_size; - u64 cert_chain_offset; - // H3Size is always 0x18000 - u64 h3_offset; - u64 data_offset; - u64 data_size; - // TMD would be here - u64 dol_offset; - u64 dol_size; - u64 fst_offset; - u64 fst_size; - u32 apploader_size; - u32 apploader_trailer_size; - }; + static constexpr size_t CLUSTER_SIZE = 0x8000; +private: void MarkAsUsed(u64 offset, u64 size); void MarkAsUsedE(u64 partition_data_offset, u64 offset, u64 size); u64 ToClusterOffset(u64 offset) const; bool ReadFromVolume(u64 offset, u32& buffer, const Partition& partition); bool ReadFromVolume(u64 offset, u64& buffer, const Partition& partition); bool ParseDisc(); - bool ParsePartitionData(const Partition& partition, PartitionHeader* header); + bool ParsePartitionData(const Partition& partition); void ParseFileSystemData(u64 partition_data_offset, const FileInfo& directory); const Volume* m_disc; std::vector m_free_table; u64 m_file_size = 0; - u64 m_block_count = 0; - u32 m_block_size = 0; bool m_is_scrubbing = false; }; diff --git a/Source/Core/DiscIO/DriveBlob.h b/Source/Core/DiscIO/DriveBlob.h index fddfbee7a2..46c141b1d3 100644 --- a/Source/Core/DiscIO/DriveBlob.h +++ b/Source/Core/DiscIO/DriveBlob.h @@ -23,11 +23,15 @@ class DriveReader : public SectorReader public: static std::unique_ptr Create(const std::string& drive); ~DriveReader(); + BlobType GetBlobType() const override { return BlobType::DRIVE; } + u64 GetRawSize() const override { return m_size; } u64 GetDataSize() const override { return m_size; } bool IsDataSizeAccurate() const override { return true; } + u64 GetBlockSize() const override { return ECC_BLOCK_SIZE; } + private: DriveReader(const std::string& drive); bool GetBlock(u64 block_num, u8* out_ptr) override; @@ -41,6 +45,7 @@ private: File::IOFile m_file; bool IsOK() const { return m_file.IsOpen() && m_file.IsGood(); } #endif + static constexpr u64 ECC_BLOCK_SIZE = 0x8000; u64 m_size = 0; }; diff --git a/Source/Core/DiscIO/FileBlob.cpp b/Source/Core/DiscIO/FileBlob.cpp index 9e0486d969..ca0aa2cfd0 100644 --- a/Source/Core/DiscIO/FileBlob.cpp +++ b/Source/Core/DiscIO/FileBlob.cpp @@ -2,10 +2,15 @@ // Licensed under GPLv2+ // Refer to the license.txt file included. +#include #include #include #include +#include +#include "Common/Assert.h" +#include "Common/FileUtil.h" +#include "Common/MsgHandler.h" #include "DiscIO/FileBlob.h" namespace DiscIO @@ -36,4 +41,76 @@ 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, void* arg) +{ + ASSERT(infile->IsDataSizeAccurate()); + + File::IOFile outfile(outfile_path, "wb"); + if (!outfile) + { + PanicAlertT("Failed to open the output file \"%s\".\n" + "Check that you have permissions to write the target folder and that the media can " + "be written.", + outfile_path.c_str()); + return false; + } + + constexpr size_t DESIRED_BUFFER_SIZE = 0x80000; + u64 buffer_size = infile->GetBlockSize(); + if (buffer_size == 0) + { + buffer_size = DESIRED_BUFFER_SIZE; + } + else + { + while (buffer_size < DESIRED_BUFFER_SIZE) + buffer_size *= 2; + } + + std::vector buffer(buffer_size); + const u64 num_buffers = (infile->GetDataSize() + buffer_size - 1) / buffer_size; + int progress_monitor = std::max(1, num_buffers / 100); + bool success = true; + + for (u64 i = 0; i < num_buffers; i++) + { + if (i % progress_monitor == 0) + { + const bool was_cancelled = + !callback(Common::GetStringT("Unpacking"), (float)i / (float)num_buffers, arg); + if (was_cancelled) + { + success = false; + break; + } + } + const u64 inpos = i * buffer_size; + const u64 sz = std::min(buffer_size, infile->GetDataSize() - inpos); + if (!infile->Read(inpos, sz, buffer.data())) + { + PanicAlertT("Failed to read from the input file \"%s\".", infile_path.c_str()); + success = false; + break; + } + if (!outfile.WriteBytes(buffer.data(), sz)) + { + PanicAlertT("Failed to write the output file \"%s\".\n" + "Check that you have enough space available on the target drive.", + outfile_path.c_str()); + success = false; + break; + } + } + + if (!success) + { + // Remove the incomplete output file. + outfile.Close(); + File::Delete(outfile_path); + } + + return success; +} + } // namespace DiscIO diff --git a/Source/Core/DiscIO/ScrubbedBlob.cpp b/Source/Core/DiscIO/ScrubbedBlob.cpp new file mode 100644 index 0000000000..255edebf57 --- /dev/null +++ b/Source/Core/DiscIO/ScrubbedBlob.cpp @@ -0,0 +1,67 @@ +// Copyright 2020 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#include "DiscIO/ScrubbedBlob.h" + +#include +#include +#include +#include + +#include "Common/Align.h" +#include "DiscIO/Blob.h" +#include "DiscIO/DiscScrubber.h" +#include "DiscIO/Volume.h" + +namespace DiscIO +{ +ScrubbedBlob::ScrubbedBlob(std::unique_ptr blob_reader, DiscScrubber scrubber) + : m_blob_reader(std::move(blob_reader)), m_scrubber(std::move(scrubber)) +{ +} + +std::unique_ptr ScrubbedBlob::Create(const std::string& path) +{ + std::unique_ptr disc = CreateDisc(path); + if (!disc) + return nullptr; + + DiscScrubber scrubber; + if (!scrubber.SetupScrub(disc.get())) + return nullptr; + + std::unique_ptr blob = CreateBlobReader(path); + if (!blob) + return nullptr; + + return std::unique_ptr(new ScrubbedBlob(std::move(blob), std::move(scrubber))); +} + +bool ScrubbedBlob::Read(u64 offset, u64 size, u8* out_ptr) +{ + while (size > 0) + { + constexpr size_t CLUSTER_SIZE = DiscScrubber::CLUSTER_SIZE; + const u64 bytes_to_read = + std::min(Common::AlignDown(offset + CLUSTER_SIZE, CLUSTER_SIZE) - offset, size); + + if (m_scrubber.CanBlockBeScrubbed(offset)) + { + std::fill_n(out_ptr, bytes_to_read, 0); + } + else + { + if (!m_blob_reader->Read(offset, bytes_to_read, out_ptr)) + return false; + } + + offset += bytes_to_read; + size -= bytes_to_read; + out_ptr += bytes_to_read; + } + + return true; +} + +} // namespace DiscIO diff --git a/Source/Core/DiscIO/ScrubbedBlob.h b/Source/Core/DiscIO/ScrubbedBlob.h new file mode 100644 index 0000000000..b98b78681e --- /dev/null +++ b/Source/Core/DiscIO/ScrubbedBlob.h @@ -0,0 +1,37 @@ +// Copyright 2020 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#pragma once + +#include +#include + +#include "DiscIO/Blob.h" +#include "DiscIO/DiscScrubber.h" + +namespace DiscIO +{ +// This class wraps another BlobReader and zeroes out data that has been +// identified by DiscScrubber as unused. +class ScrubbedBlob : public BlobReader +{ +public: + static std::unique_ptr Create(const std::string& path); + + BlobType GetBlobType() const override { return m_blob_reader->GetBlobType(); } + 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(); } + u64 GetBlockSize() const override { return m_blob_reader->GetBlockSize(); } + + bool Read(u64 offset, u64 size, u8* out_ptr) override; + +private: + ScrubbedBlob(std::unique_ptr blob_reader, DiscScrubber scrubber); + + std::unique_ptr m_blob_reader; + DiscScrubber m_scrubber; +}; + +} // namespace DiscIO diff --git a/Source/Core/DiscIO/VolumeVerifier.cpp b/Source/Core/DiscIO/VolumeVerifier.cpp index 354e843e92..38c6fd2e35 100644 --- a/Source/Core/DiscIO/VolumeVerifier.cpp +++ b/Source/Core/DiscIO/VolumeVerifier.cpp @@ -1040,7 +1040,7 @@ void VolumeVerifier::SetUpHashing() else if (m_volume.GetVolumeType() == Platform::WiiDisc) { // Set up a DiscScrubber for checking whether blocks with errors are unused - m_scrubber.SetupScrub(&m_volume, VolumeWii::BLOCK_TOTAL_SIZE); + m_scrubber.SetupScrub(&m_volume); } std::sort(m_blocks.begin(), m_blocks.end(), diff --git a/Source/Core/DiscIO/WbfsBlob.h b/Source/Core/DiscIO/WbfsBlob.h index 36de02f62d..0283b1c98b 100644 --- a/Source/Core/DiscIO/WbfsBlob.h +++ b/Source/Core/DiscIO/WbfsBlob.h @@ -24,6 +24,7 @@ public: static std::unique_ptr Create(File::IOFile file, const std::string& path); BlobType GetBlobType() const override { return BlobType::WBFS; } + u64 GetRawSize() const override { return m_size; } // The WBFS format does not save the original file size. // This function returns a constant upper bound @@ -31,6 +32,8 @@ public: u64 GetDataSize() const override; bool IsDataSizeAccurate() const override { return false; } + u64 GetBlockSize() const override { return m_wbfs_sector_size; } + bool Read(u64 offset, u64 nbytes, u8* out_ptr) override; private: diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt index 4589d4881f..8449920c61 100644 --- a/Source/Core/DolphinQt/CMakeLists.txt +++ b/Source/Core/DolphinQt/CMakeLists.txt @@ -18,6 +18,8 @@ add_executable(dolphin-emu AboutDialog.h CheatsManager.cpp CheatsManager.h + ConvertDialog.cpp + ConvertDialog.h DiscordHandler.cpp DiscordHandler.h DiscordJoinRequestDialog.cpp diff --git a/Source/Core/DolphinQt/ConvertDialog.cpp b/Source/Core/DolphinQt/ConvertDialog.cpp new file mode 100644 index 0000000000..44f41e757e --- /dev/null +++ b/Source/Core/DolphinQt/ConvertDialog.cpp @@ -0,0 +1,374 @@ +// Copyright 2020 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#include "DolphinQt/ConvertDialog.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Common/Assert.h" +#include "Common/Logging/Log.h" +#include "DiscIO/Blob.h" +#include "DiscIO/ScrubbedBlob.h" +#include "DolphinQt/QtUtils/ModalMessageBox.h" +#include "DolphinQt/QtUtils/ParallelProgressDialog.h" +#include "UICommon/GameFile.h" +#include "UICommon/UICommon.h" + +static bool CompressCB(const std::string& text, float percent, void* ptr) +{ + if (ptr == nullptr) + return false; + + auto* progress_dialog = static_cast(ptr); + + progress_dialog->SetValue(percent * 100); + return !progress_dialog->WasCanceled(); +} + +ConvertDialog::ConvertDialog(QList> files, + QWidget* parent) + : QDialog(parent), m_files(std::move(files)) +{ + ASSERT(!m_files.empty()); + + setWindowTitle(tr("Convert")); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + QGridLayout* grid_layout = new QGridLayout; + grid_layout->setColumnStretch(1, 1); + + m_format = new QComboBox; + AddToFormatComboBox(QStringLiteral("ISO"), DiscIO::BlobType::PLAIN); + AddToFormatComboBox(QStringLiteral("GCZ"), DiscIO::BlobType::GCZ); + grid_layout->addWidget(new QLabel(tr("Format:")), 0, 0); + grid_layout->addWidget(m_format, 0, 1); + + m_block_size = new QComboBox; + grid_layout->addWidget(new QLabel(tr("Block Size:")), 1, 0); + grid_layout->addWidget(m_block_size, 1, 1); + + m_scrub = new QCheckBox; + grid_layout->addWidget(new QLabel(tr("Remove Junk Data (Irreversible):")), 2, 0); + grid_layout->addWidget(m_scrub, 2, 1); + m_scrub->setEnabled( + std::none_of(m_files.begin(), m_files.end(), std::mem_fn(&UICommon::GameFile::IsDatelDisc))); + + QPushButton* convert_button = new QPushButton(tr("Convert")); + + QVBoxLayout* options_layout = new QVBoxLayout; + options_layout->addLayout(grid_layout); + options_layout->addWidget(convert_button); + QGroupBox* options_group = new QGroupBox(tr("Options")); + options_group->setLayout(options_layout); + + QLabel* info_text = + new QLabel(tr("ISO: A simple and robust format which is supported by many programs. " + "It takes up more space than any other format.\n\n" + "GCZ: A basic compressed format which is compatible with most versions of " + "Dolphin and some other programs. It can't efficiently compress junk data " + "(unless removed) or encrypted Wii data.")); + info_text->setWordWrap(true); + + QVBoxLayout* info_layout = new QVBoxLayout; + info_layout->addWidget(info_text); + QGroupBox* info_group = new QGroupBox(tr("Info")); + info_group->setLayout(info_layout); + + QVBoxLayout* main_layout = new QVBoxLayout; + main_layout->addWidget(options_group); + main_layout->addWidget(info_group); + + setLayout(main_layout); + + connect(m_format, QOverload::of(&QComboBox::currentIndexChanged), this, + &ConvertDialog::OnFormatChanged); + connect(convert_button, &QPushButton::clicked, this, &ConvertDialog::Convert); + + OnFormatChanged(); +} + +void ConvertDialog::AddToFormatComboBox(const QString& name, DiscIO::BlobType format) +{ + if (std::all_of(m_files.begin(), m_files.end(), + [format](const auto& file) { return file->GetBlobType() == format; })) + { + return; + } + + m_format->addItem(name, static_cast(format)); +} + +void ConvertDialog::AddToBlockSizeComboBox(int size) +{ + m_block_size->addItem(QString::fromStdString(UICommon::FormatSize(size, 0)), size); +} + +void ConvertDialog::OnFormatChanged() +{ + // Because DVD timings are emulated as if we can't read less than an entire ECC block at once + // (32 KiB - 0x8000), there is little reason to use a block size smaller than that. + constexpr int MIN_BLOCK_SIZE = 0x8000; + + // For performance reasons, blocks shouldn't be too large. + // 2 MiB (0x200000) was picked because it is the smallest block size supported by WIA. + constexpr int MAX_BLOCK_SIZE = 0x200000; + + const DiscIO::BlobType format = static_cast(m_format->currentData().toInt()); + + m_block_size->clear(); + switch (format) + { + case DiscIO::BlobType::GCZ: + { + m_block_size->setEnabled(true); + + // In order for versions of Dolphin prior to 5.0-11893 to be able to convert a GCZ file + // to ISO without messing up the final part of the file in some way, the file size + // must be an integer multiple of the block size (fixed in 3aa463c) and must not be + // an integer multiple of the block size multiplied by 32 (fixed in 26b21e3). + + const auto block_size_ok = [this](int block_size) { + return std::all_of(m_files.begin(), m_files.end(), [block_size](const auto& file) { + constexpr u64 BLOCKS_PER_BUFFER = 32; + const u64 file_size = file->GetVolumeSize(); + return file_size % block_size == 0 && file_size % (block_size * BLOCKS_PER_BUFFER) != 0; + }); + }; + + // Add all block sizes in the normal range that do not cause problems + for (int block_size = MIN_BLOCK_SIZE; block_size <= MAX_BLOCK_SIZE; block_size *= 2) + { + if (block_size_ok(block_size)) + AddToBlockSizeComboBox(block_size); + } + + // If we didn't find a good block size, pick the block size which was hardcoded + // in older versions of Dolphin. That way, at least we're not worse than older versions. + if (m_block_size->count() == 0) + { + constexpr int FALLBACK_BLOCK_SIZE = 0x4000; + if (!block_size_ok(FALLBACK_BLOCK_SIZE)) + { + ERROR_LOG(MASTER_LOG, "Failed to find a block size which does not cause problems " + "when decompressing using an old version of Dolphin"); + } + AddToBlockSizeComboBox(FALLBACK_BLOCK_SIZE); + } + + break; + } + default: + m_block_size->setEnabled(false); + break; + } +} + +bool ConvertDialog::ShowAreYouSureDialog(const QString& text) +{ + ModalMessageBox warning(this); + warning.setIcon(QMessageBox::Warning); + warning.setWindowTitle(tr("Confirm")); + warning.setText(tr("Are you sure?")); + warning.setInformativeText(text); + warning.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + + return warning.exec() == QMessageBox::Yes; +} + +void ConvertDialog::Convert() +{ + const DiscIO::BlobType format = static_cast(m_format->currentData().toInt()); + const int block_size = m_block_size->currentData().toInt(); + const bool scrub = m_scrub->isChecked(); + + if (scrub && format == DiscIO::BlobType::PLAIN) + { + if (!ShowAreYouSureDialog(tr("Removing junk data does not save any space when converting to " + "ISO (unless you package the ISO file in a compressed file format " + "such as ZIP afterwards). Do you want to continue anyway?"))) + { + return; + } + } + + if (!scrub && format == DiscIO::BlobType::GCZ && + std::any_of(m_files.begin(), m_files.end(), [](const auto& file) { + return file->GetPlatform() == DiscIO::Platform::WiiDisc && !file->IsDatelDisc(); + })) + { + if (!ShowAreYouSureDialog(tr("Converting Wii disc images to GCZ without removing junk data " + "does not save any noticeable amount of space compared to " + "converting to ISO. Do you want to continue anyway?"))) + { + return; + } + } + + QString extension; + QString filter; + switch (format) + { + case DiscIO::BlobType::PLAIN: + extension = QStringLiteral(".iso"); + filter = tr("Uncompressed GC/Wii images (*.iso *.gcm)"); + break; + case DiscIO::BlobType::GCZ: + extension = QStringLiteral(".gcz"); + filter = tr("Compressed GC/Wii images (*.gcz)"); + break; + default: + ASSERT(false); + return; + } + + QString dst_dir; + QString dst_path; + + if (m_files.size() > 1) + { + dst_dir = QFileDialog::getExistingDirectory( + this, tr("Select where you want to save the converted images"), + QFileInfo(QString::fromStdString(m_files[0]->GetFilePath())).dir().absolutePath()); + + if (dst_dir.isEmpty()) + return; + } + else + { + dst_path = QFileDialog::getSaveFileName( + this, tr("Select where you want to save the converted image"), + QFileInfo(QString::fromStdString(m_files[0]->GetFilePath())) + .dir() + .absoluteFilePath( + QFileInfo(QString::fromStdString(m_files[0]->GetFilePath())).completeBaseName()) + .append(extension), + filter); + + if (dst_path.isEmpty()) + return; + } + + for (const auto& file : m_files) + { + const auto original_path = file->GetFilePath(); + if (m_files.size() > 1) + { + dst_path = + QDir(dst_dir) + .absoluteFilePath(QFileInfo(QString::fromStdString(original_path)).completeBaseName()) + .append(extension); + QFileInfo dst_info = QFileInfo(dst_path); + if (dst_info.exists()) + { + ModalMessageBox confirm_replace(this); + confirm_replace.setIcon(QMessageBox::Warning); + confirm_replace.setWindowTitle(tr("Confirm")); + confirm_replace.setText(tr("The file %1 already exists.\n" + "Do you wish to replace it?") + .arg(dst_info.fileName())); + confirm_replace.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + + if (confirm_replace.exec() == QMessageBox::No) + continue; + } + } + + ParallelProgressDialog progress_dialog(tr("Converting..."), tr("Abort"), 0, 100, this); + progress_dialog.GetRaw()->setWindowModality(Qt::WindowModal); + progress_dialog.GetRaw()->setWindowTitle(tr("Progress")); + + if (m_files.size() > 1) + { + progress_dialog.GetRaw()->setLabelText( + tr("Converting...") + QLatin1Char{'\n'} + + QFileInfo(QString::fromStdString(original_path)).fileName()); + } + + std::unique_ptr blob_reader; + bool scrub_current_file = scrub; + + if (scrub_current_file) + { + blob_reader = DiscIO::ScrubbedBlob::Create(original_path); + if (!blob_reader) + { + const int result = + ModalMessageBox::warning(this, tr("Question"), + tr("Failed to remove junk data from file \"%1\".\n\n" + "Would you like to convert it without removing junk data?") + .arg(QString::fromStdString(original_path)), + QMessageBox::Ok | QMessageBox::Abort); + + if (result == QMessageBox::Ok) + scrub_current_file = false; + else + return; + } + } + + if (!scrub_current_file) + blob_reader = DiscIO::CreateBlobReader(original_path); + + if (!blob_reader) + { + QErrorMessage(this).showMessage( + tr("Failed to open the input file \"%1\".").arg(QString::fromStdString(original_path))); + } + else + { + std::future good; + + if (format == DiscIO::BlobType::PLAIN) + { + good = std::async(std::launch::async, [&] { + const bool good = + DiscIO::ConvertToPlain(blob_reader.get(), original_path, dst_path.toStdString(), + &CompressCB, &progress_dialog); + progress_dialog.Reset(); + return good; + }); + } + else if (format == DiscIO::BlobType::GCZ) + { + good = std::async(std::launch::async, [&] { + const bool good = + DiscIO::ConvertToGCZ(blob_reader.get(), original_path, dst_path.toStdString(), + file->GetPlatform() == DiscIO::Platform::WiiDisc ? 1 : 0, + block_size, &CompressCB, &progress_dialog); + progress_dialog.Reset(); + return good; + }); + } + + progress_dialog.GetRaw()->exec(); + if (!good.get()) + { + QErrorMessage(this).showMessage(tr("Dolphin failed to complete the requested action.")); + return; + } + } + } + + ModalMessageBox::information(this, tr("Success"), + tr("Successfully converted %n image(s).", "", m_files.size())); + + close(); +} diff --git a/Source/Core/DolphinQt/ConvertDialog.h b/Source/Core/DolphinQt/ConvertDialog.h new file mode 100644 index 0000000000..1ff80b4a07 --- /dev/null +++ b/Source/Core/DolphinQt/ConvertDialog.h @@ -0,0 +1,44 @@ +// Copyright 2020 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#pragma once + +#include + +#include +#include + +#include "DiscIO/Blob.h" + +class QCheckBox; +class QComboBox; + +namespace UICommon +{ +class GameFile; +} + +class ConvertDialog final : public QDialog +{ + Q_OBJECT + +public: + explicit ConvertDialog(QList> files, + QWidget* parent = nullptr); + +private slots: + void OnFormatChanged(); + void Convert(); + +private: + void AddToFormatComboBox(const QString& name, DiscIO::BlobType format); + void AddToBlockSizeComboBox(int size); + + bool ShowAreYouSureDialog(const QString& text); + + QComboBox* m_format; + QComboBox* m_block_size; + QCheckBox* m_scrub; + QList> m_files; +}; diff --git a/Source/Core/DolphinQt/DolphinQt.vcxproj b/Source/Core/DolphinQt/DolphinQt.vcxproj index c6dccf12c1..023f50a5e1 100644 --- a/Source/Core/DolphinQt/DolphinQt.vcxproj +++ b/Source/Core/DolphinQt/DolphinQt.vcxproj @@ -131,6 +131,7 @@ + @@ -212,6 +213,7 @@ + @@ -374,6 +376,7 @@ + diff --git a/Source/Core/DolphinQt/GameList/GameList.cpp b/Source/Core/DolphinQt/GameList/GameList.cpp index 28922ecc0b..12c7cacc37 100644 --- a/Source/Core/DolphinQt/GameList/GameList.cpp +++ b/Source/Core/DolphinQt/GameList/GameList.cpp @@ -6,7 +6,7 @@ #include #include -#include +#include #include #include @@ -40,6 +40,7 @@ #include "DiscIO/Enums.h" #include "DolphinQt/Config/PropertiesDialog.h" +#include "DolphinQt/ConvertDialog.h" #include "DolphinQt/GameList/GameListModel.h" #include "DolphinQt/GameList/GridProxyModel.h" #include "DolphinQt/GameList/ListProxyModel.h" @@ -53,8 +54,6 @@ #include "UICommon/GameFile.h" -static bool CompressCB(const std::string&, float, void*); - GameList::GameList(QWidget* parent) : QStackedWidget(parent) { m_model = Settings::Instance().GetGameListModel(); @@ -257,35 +256,16 @@ void GameList::ShowContextMenu(const QPoint&) if (HasMultipleSelected()) { - bool wii_saves = true; - bool compress = false; - bool decompress = false; - - for (const auto& game : GetSelectedGames()) + if (std::all_of(GetSelectedGames().begin(), GetSelectedGames().end(), [](const auto& game) { + return DiscIO::IsDisc(game->GetPlatform()) && game->IsVolumeSizeAccurate(); + })) { - DiscIO::Platform platform = game->GetPlatform(); - - if (platform == DiscIO::Platform::GameCubeDisc || platform == DiscIO::Platform::WiiDisc) - { - const auto blob_type = game->GetBlobType(); - if (blob_type == DiscIO::BlobType::GCZ) - decompress = true; - else if (blob_type == DiscIO::BlobType::PLAIN) - compress = true; - } - - if (platform != DiscIO::Platform::WiiWAD && platform != DiscIO::Platform::WiiDisc) - wii_saves = false; + menu->addAction(tr("Convert Selected Files..."), this, &GameList::ConvertFile); + menu->addSeparator(); } - if (compress) - menu->addAction(tr("Compress Selected ISOs..."), this, [this] { CompressISO(false); }); - if (decompress) - menu->addAction(tr("Decompress Selected ISOs..."), this, [this] { CompressISO(true); }); - if (compress || decompress) - menu->addSeparator(); - - if (wii_saves) + if (std::all_of(GetSelectedGames().begin(), GetSelectedGames().end(), + [](const auto& game) { return DiscIO::IsWii(game->GetPlatform()); })) { menu->addAction(tr("Export Wii Saves"), this, &GameList::ExportWiiSave); menu->addSeparator(); @@ -306,15 +286,13 @@ void GameList::ShowContextMenu(const QPoint&) menu->addSeparator(); } - if (platform == DiscIO::Platform::GameCubeDisc || platform == DiscIO::Platform::WiiDisc) + if (DiscIO::IsDisc(platform)) { menu->addAction(tr("Set as &Default ISO"), this, &GameList::SetDefaultISO); const auto blob_type = game->GetBlobType(); - if (blob_type == DiscIO::BlobType::GCZ) - menu->addAction(tr("Decompress ISO..."), this, [this] { CompressISO(true); }); - else if (blob_type == DiscIO::BlobType::PLAIN) - menu->addAction(tr("Compress ISO..."), this, [this] { CompressISO(false); }); + if (game->IsVolumeSizeAccurate()) + menu->addAction(tr("Convert File..."), this, &GameList::ConvertFile); QAction* change_disc = menu->addAction(tr("Change &Disc"), this, &GameList::ChangeDisc); @@ -481,157 +459,14 @@ void GameList::OpenWiki() QDesktopServices::openUrl(QUrl(url)); } -void GameList::CompressISO(bool decompress) +void GameList::ConvertFile() { - auto files = GetSelectedGames(); - const auto game = GetSelectedGame(); - - if (files.empty() || !game) + auto games = GetSelectedGames(); + if (games.empty()) return; - bool wii_warning_given = false; - for (QMutableListIterator> it(files); it.hasNext();) - { - auto file = it.next(); - - if ((file->GetPlatform() != DiscIO::Platform::GameCubeDisc && - file->GetPlatform() != DiscIO::Platform::WiiDisc) || - (decompress && file->GetBlobType() != DiscIO::BlobType::GCZ) || - (!decompress && file->GetBlobType() != DiscIO::BlobType::PLAIN)) - { - it.remove(); - continue; - } - - if (!wii_warning_given && !decompress && file->GetPlatform() == DiscIO::Platform::WiiDisc) - { - ModalMessageBox wii_warning(this); - wii_warning.setIcon(QMessageBox::Warning); - wii_warning.setWindowTitle(tr("Confirm")); - wii_warning.setText(tr("Are you sure?")); - wii_warning.setInformativeText(tr( - "Compressing a Wii disc image will irreversibly change the compressed copy by removing " - "padding data. Your disc image will still work. Continue?")); - wii_warning.setStandardButtons(QMessageBox::Yes | QMessageBox::No); - - if (wii_warning.exec() == QMessageBox::No) - return; - - wii_warning_given = true; - } - } - - QString dst_dir; - QString dst_path; - - if (files.size() > 1) - { - dst_dir = QFileDialog::getExistingDirectory( - this, - decompress ? tr("Select where you want to save the decompressed images") : - tr("Select where you want to save the compressed images"), - QFileInfo(QString::fromStdString(game->GetFilePath())).dir().absolutePath()); - - if (dst_dir.isEmpty()) - return; - } - else - { - dst_path = QFileDialog::getSaveFileName( - this, - decompress ? tr("Select where you want to save the decompressed image") : - tr("Select where you want to save the compressed image"), - QFileInfo(QString::fromStdString(game->GetFilePath())) - .dir() - .absoluteFilePath( - QFileInfo(QString::fromStdString(files[0]->GetFilePath())).completeBaseName()) - .append(decompress ? QStringLiteral(".gcm") : QStringLiteral(".gcz")), - decompress ? tr("Uncompressed GC/Wii images (*.iso *.gcm)") : - tr("Compressed GC/Wii images (*.gcz)")); - - if (dst_path.isEmpty()) - return; - } - - for (const auto& file : files) - { - const auto original_path = file->GetFilePath(); - if (files.size() > 1) - { - dst_path = - QDir(dst_dir) - .absoluteFilePath(QFileInfo(QString::fromStdString(original_path)).completeBaseName()) - .append(decompress ? QStringLiteral(".gcm") : QStringLiteral(".gcz")); - QFileInfo dst_info = QFileInfo(dst_path); - if (dst_info.exists()) - { - ModalMessageBox confirm_replace(this); - confirm_replace.setIcon(QMessageBox::Warning); - confirm_replace.setWindowTitle(tr("Confirm")); - confirm_replace.setText(tr("The file %1 already exists.\n" - "Do you wish to replace it?") - .arg(dst_info.fileName())); - confirm_replace.setStandardButtons(QMessageBox::Yes | QMessageBox::No); - - if (confirm_replace.exec() == QMessageBox::No) - continue; - } - } - - ParallelProgressDialog progress_dialog( - decompress ? tr("Decompressing...") : tr("Compressing..."), tr("Abort"), 0, 100, this); - progress_dialog.GetRaw()->setWindowModality(Qt::WindowModal); - progress_dialog.GetRaw()->setWindowTitle(tr("Progress")); - - std::future good; - - if (decompress) - { - if (files.size() > 1) - { - progress_dialog.GetRaw()->setLabelText( - tr("Decompressing...") + QLatin1Char{'\n'} + - QFileInfo(QString::fromStdString(original_path)).fileName()); - } - - good = std::async(std::launch::async, [&] { - const bool good = DiscIO::DecompressBlobToFile(original_path, dst_path.toStdString(), - &CompressCB, &progress_dialog); - progress_dialog.Reset(); - return good; - }); - } - else - { - if (files.size() > 1) - { - progress_dialog.GetRaw()->setLabelText( - tr("Compressing...") + QLatin1Char{'\n'} + - QFileInfo(QString::fromStdString(original_path)).fileName()); - } - - good = std::async(std::launch::async, [&] { - const bool good = - DiscIO::CompressFileToBlob(original_path, dst_path.toStdString(), - file->GetPlatform() == DiscIO::Platform::WiiDisc ? 1 : 0, - 16384, &CompressCB, &progress_dialog); - progress_dialog.Reset(); - return good; - }); - } - - progress_dialog.GetRaw()->exec(); - if (!good.get()) - { - QErrorMessage(this).showMessage(tr("Dolphin failed to complete the requested action.")); - return; - } - } - - ModalMessageBox::information(this, tr("Success"), - decompress ? - tr("Successfully decompressed %n image(s).", "", files.size()) : - tr("Successfully compressed %n image(s).", "", files.size())); + ConvertDialog dialog{std::move(games), this}; + dialog.exec(); } void GameList::InstallWAD() @@ -953,17 +788,6 @@ void GameList::OnGameListVisibilityChanged() m_grid_proxy->invalidate(); } -static bool CompressCB(const std::string& text, float percent, void* ptr) -{ - if (ptr == nullptr) - return false; - - auto* progress_dialog = static_cast(ptr); - - progress_dialog->SetValue(percent * 100); - return !progress_dialog->WasCanceled(); -} - void GameList::OnSectionResized(int index, int, int) { auto* hor_header = m_list->horizontalHeader(); diff --git a/Source/Core/DolphinQt/GameList/GameList.h b/Source/Core/DolphinQt/GameList/GameList.h index 5192e89230..f64ddd61f0 100644 --- a/Source/Core/DolphinQt/GameList/GameList.h +++ b/Source/Core/DolphinQt/GameList/GameList.h @@ -64,7 +64,7 @@ private: void InstallWAD(); void UninstallWAD(); void ExportWiiSave(); - void CompressISO(bool decompress); + void ConvertFile(); void ChangeDisc(); void NewTag(); void DeleteTag(); diff --git a/Source/Core/UICommon/GameFile.cpp b/Source/Core/UICommon/GameFile.cpp index cc64d9d3e9..ca5c53ae31 100644 --- a/Source/Core/UICommon/GameFile.cpp +++ b/Source/Core/UICommon/GameFile.cpp @@ -38,6 +38,7 @@ #include "Core/TitleDatabase.h" #include "DiscIO/Blob.h" +#include "DiscIO/DiscExtractor.h" #include "DiscIO/Enums.h" #include "DiscIO/Volume.h" #include "DiscIO/WiiSaveBanner.h" @@ -116,6 +117,9 @@ GameFile::GameFile(std::string path) : m_file_path(std::move(path)) m_blob_type = volume->GetBlobType(); m_file_size = volume->GetRawSize(); m_volume_size = volume->GetSize(); + m_volume_size_is_accurate = volume->IsSizeAccurate(); + m_is_datel_disc = DiscIO::IsDisc(m_platform) && + !DiscIO::GetBootDOLOffset(*volume, volume->GetGamePartition()); m_internal_name = volume->GetInternalName(); m_game_id = volume->GetGameID(); @@ -136,6 +140,8 @@ 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_volume_size_is_accurate = true; + m_is_datel_disc = false; m_platform = DiscIO::Platform::ELFOrDOL; m_blob_type = DiscIO::BlobType::DIRECTORY; } @@ -296,6 +302,8 @@ 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_is_datel_disc); p.Do(m_short_names); p.Do(m_long_names); diff --git a/Source/Core/UICommon/GameFile.h b/Source/Core/UICommon/GameFile.h index 87513118a9..e3c29860f4 100644 --- a/Source/Core/UICommon/GameFile.h +++ b/Source/Core/UICommon/GameFile.h @@ -89,6 +89,8 @@ 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; } + bool IsDatelDisc() const { return m_is_datel_disc; } const GameBanner& GetBannerImage() const; const GameCover& GetCoverImage() const; void DoState(PointerWrap& p); @@ -124,6 +126,8 @@ private: u64 m_file_size{}; u64 m_volume_size{}; + bool m_volume_size_is_accurate{}; + bool m_is_datel_disc{}; std::map m_short_names; std::map m_long_names; diff --git a/Source/Core/UICommon/GameFileCache.cpp b/Source/Core/UICommon/GameFileCache.cpp index 07cbf726c4..9b2729407e 100644 --- a/Source/Core/UICommon/GameFileCache.cpp +++ b/Source/Core/UICommon/GameFileCache.cpp @@ -27,7 +27,7 @@ namespace UICommon { -static constexpr u32 CACHE_REVISION = 16; // Last changed in PR 8313 +static constexpr u32 CACHE_REVISION = 17; // Last changed in PR 8738 std::vector FindAllGamePaths(const std::vector& directories_to_scan, bool recursive_scan) diff --git a/Source/Core/UICommon/UICommon.cpp b/Source/Core/UICommon/UICommon.cpp index 5af9192711..adff5adca3 100644 --- a/Source/Core/UICommon/UICommon.cpp +++ b/Source/Core/UICommon/UICommon.cpp @@ -453,7 +453,7 @@ void EnableScreenSaver(bool enable) #endif } -std::string FormatSize(u64 bytes) +std::string FormatSize(u64 bytes, int decimals) { // i18n: The symbol for the unit "bytes" const char* const unit_symbols[] = {_trans("B"), _trans("KiB"), _trans("MiB"), _trans("GiB"), @@ -468,7 +468,7 @@ std::string FormatSize(u64 bytes) // Don't need exact values, only 5 most significant digits const double unit_size = std::pow(2, unit * 10); std::ostringstream ss; - ss << std::fixed << std::setprecision(2); + ss << std::fixed << std::setprecision(decimals); ss << bytes / unit_size << ' ' << Common::GetStringT(unit_symbols[unit]); return ss.str(); } diff --git a/Source/Core/UICommon/UICommon.h b/Source/Core/UICommon/UICommon.h index b7169bfb15..2722559a2a 100644 --- a/Source/Core/UICommon/UICommon.h +++ b/Source/Core/UICommon/UICommon.h @@ -32,5 +32,5 @@ void SaveWiimoteSources(); // Return a pretty file size string from byte count. // e.g. 1134278 -> "1.08 MiB" -std::string FormatSize(u64 bytes); +std::string FormatSize(u64 bytes, int decimals = 2); } // namespace UICommon