diff --git a/CMakeLists.txt b/CMakeLists.txt index b076ae7210..5315c67845 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,6 +32,10 @@ if(NOT WIN32 AND NOT APPLE AND NOT HAIKU) option(ENABLE_EGL "Enables EGL OpenGL Interface" ON) endif() +if(NOT ANDROID) + option(ENABLE_CLI_TOOL "Enable dolphin-tool, a CLI-based utility for functions such as managing disc images" ON) +endif() + option(USE_SHARED_ENET "Use shared libenet if found rather than Dolphin's soon-to-compatibly-diverge version" OFF) option(USE_UPNP "Enables UPnP port mapping support" ON) option(ENABLE_NOGUI "Enable NoGUI frontend" ON) diff --git a/Source/Core/CMakeLists.txt b/Source/Core/CMakeLists.txt index d176fcbf25..8dd46c1602 100644 --- a/Source/Core/CMakeLists.txt +++ b/Source/Core/CMakeLists.txt @@ -11,6 +11,10 @@ if(ENABLE_NOGUI) add_subdirectory(DolphinNoGUI) endif() +if(ENABLE_CLI_TOOL) + add_subdirectory(DolphinTool) +endif() + if(ENABLE_QT) add_subdirectory(DolphinQt) endif() @@ -23,7 +27,6 @@ if (APPLE) add_subdirectory(MacUpdater) endif() - if (WIN32) add_subdirectory(WinUpdater) endif() diff --git a/Source/Core/DiscIO/DiscUtils.cpp b/Source/Core/DiscIO/DiscUtils.cpp index 84cf7ed7c4..002ce066db 100644 --- a/Source/Core/DiscIO/DiscUtils.cpp +++ b/Source/Core/DiscIO/DiscUtils.cpp @@ -12,6 +12,8 @@ #include #include "Common/CommonTypes.h" +#include "Common/MathUtil.h" +#include "DiscIO/Blob.h" #include "DiscIO/Filesystem.h" #include "DiscIO/Volume.h" @@ -198,4 +200,49 @@ u64 GetBiggestReferencedOffset(const Volume& volume, const std::vector RVZ_BIG_BLOCK_SIZE_LCM && block_size % RVZ_BIG_BLOCK_SIZE_LCM != 0)) + { + return false; + } + + break; + default: + ASSERT(false); + break; + } + + return true; +} + } // namespace DiscIO diff --git a/Source/Core/DiscIO/DiscUtils.h b/Source/Core/DiscIO/DiscUtils.h index 0b0cb80684..5baf5fcafa 100644 --- a/Source/Core/DiscIO/DiscUtils.h +++ b/Source/Core/DiscIO/DiscUtils.h @@ -8,6 +8,7 @@ #include #include "Common/CommonTypes.h" +#include "DiscIO/Blob.h" namespace DiscIO { @@ -49,6 +50,31 @@ constexpr u32 WII_NONPARTITION_DISCHEADER_SIZE = 0x100; constexpr u32 WII_REGION_DATA_ADDRESS = 0x4E000; constexpr u32 WII_REGION_DATA_SIZE = 0x20; +// 128 KiB (0x20000) is the default block size for GCZ/RVZ images +constexpr int GCZ_RVZ_PREFERRED_BLOCK_SIZE = 0x20000; + +// 32 KiB (0x8000) was picked because DVD timings are emulated as if we can't read less than +// an entire ECC block at once. Therefore, little reason to choose a smaller block size. +constexpr int PREFERRED_MIN_BLOCK_SIZE = 0x8000; + +// 2 MiB (0x200000) was picked because it is the smallest block size supported by WIA. +// For performance reasons, blocks shouldn't be too large. +constexpr int PREFERRED_MAX_BLOCK_SIZE = 0x200000; + +// If we didn't find a good GCZ block size, pick the block size which was hardcoded +// in legacy versions. That way, at least we're not worse than older versions. +// 16 KiB (0x4000) for supporting GCZs in versions of Dolphin prior to 5.0-11893 +constexpr int GCZ_FALLBACK_BLOCK_SIZE = 0x4000; + +// 2 MiB (0x200000) is the smallest block size supported by WIA. +constexpr int WIA_MIN_BLOCK_SIZE = 0x200000; + +// 32 KiB (0x8000) is the smallest block size supported by RVZ. +constexpr int RVZ_MIN_BLOCK_SIZE = 0x8000; + +// 2 MiB (0x200000): for RVZ, block sizes larger than 2 MiB must be an integer multiple of 2 MiB. +constexpr int RVZ_BIG_BLOCK_SIZE_LCM = 0x200000; + std::string NameForPartitionType(u32 partition_type, bool include_prefix); std::optional GetApploaderSize(const Volume& volume, const Partition& partition); @@ -59,4 +85,7 @@ std::optional GetFSTSize(const Volume& volume, const Partition& partition); u64 GetBiggestReferencedOffset(const Volume& volume); u64 GetBiggestReferencedOffset(const Volume& volume, const std::vector& partitions); + +bool IsGCZBlockSizeLegacyCompatible(int block_size, u64 file_size); +bool IsDiscImageBlockSizeValid(int block_size, DiscIO::BlobType format); } // namespace DiscIO diff --git a/Source/Core/DolphinQt/ConvertDialog.cpp b/Source/Core/DolphinQt/ConvertDialog.cpp index 1777ef8e04..afc0f6e87c 100644 --- a/Source/Core/DolphinQt/ConvertDialog.cpp +++ b/Source/Core/DolphinQt/ConvertDialog.cpp @@ -23,6 +23,7 @@ #include "Common/Assert.h" #include "Common/Logging/Log.h" #include "DiscIO/Blob.h" +#include "DiscIO/DiscUtils.h" #include "DiscIO/ScrubbedBlob.h" #include "DiscIO/WIABlob.h" #include "DolphinQt/QtUtils/DolphinFileDialog.h" @@ -118,10 +119,9 @@ void ConvertDialog::AddToBlockSizeComboBox(int size) { m_block_size->addItem(QString::fromStdString(UICommon::FormatSize(size, 0)), size); - // Select 128 KiB by default, or if it is not available, the size closest to it. + // Select the default, or if it is not available, the size closest to it. // This code assumes that sizes get added to the combo box in increasing order. - constexpr int DEFAULT_SIZE = 0x20000; - if (size <= DEFAULT_SIZE) + if (size <= DiscIO::GCZ_RVZ_PREFERRED_BLOCK_SIZE) m_block_size->setCurrentIndex(m_block_size->count() - 1); } @@ -138,14 +138,6 @@ void ConvertDialog::AddToCompressionLevelComboBox(int level) 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(); @@ -156,21 +148,17 @@ void ConvertDialog::OnFormatChanged() { case DiscIO::BlobType::GCZ: { - // 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). - + // To support legacy versions of dolphin, we have to check the GCZ block size + // See DiscIO::IsGCZBlockSizeLegacyCompatible() for details 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; + return DiscIO::IsGCZBlockSizeLegacyCompatible(block_size, file->GetVolumeSize()); }); }; // 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) + for (int block_size = DiscIO::PREFERRED_MIN_BLOCK_SIZE; + block_size <= DiscIO::PREFERRED_MAX_BLOCK_SIZE; block_size *= 2) { if (block_size_ok(block_size)) AddToBlockSizeComboBox(block_size); @@ -180,13 +168,12 @@ void ConvertDialog::OnFormatChanged() // 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)) + if (!block_size_ok(DiscIO::GCZ_FALLBACK_BLOCK_SIZE)) { ERROR_LOG_FMT(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); + AddToBlockSizeComboBox(DiscIO::GCZ_FALLBACK_BLOCK_SIZE); } break; @@ -195,13 +182,14 @@ void ConvertDialog::OnFormatChanged() m_block_size->setEnabled(true); // This is the smallest block size supported by WIA. For performance, larger sizes are avoided. - AddToBlockSizeComboBox(0x200000); + AddToBlockSizeComboBox(DiscIO::WIA_MIN_BLOCK_SIZE); break; case DiscIO::BlobType::RVZ: m_block_size->setEnabled(true); - for (int block_size = MIN_BLOCK_SIZE; block_size <= MAX_BLOCK_SIZE; block_size *= 2) + for (int block_size = DiscIO::PREFERRED_MIN_BLOCK_SIZE; + block_size <= DiscIO::PREFERRED_MAX_BLOCK_SIZE; block_size *= 2) AddToBlockSizeComboBox(block_size); break; @@ -257,6 +245,7 @@ void ConvertDialog::OnFormatChanged() m_block_size->setEnabled(m_block_size->count() > 1); m_compression->setEnabled(m_compression->count() > 1); + // Block scrubbing of RVZ containers and Datel discs const bool scrubbing_allowed = format != DiscIO::BlobType::RVZ && std::none_of(m_files.begin(), m_files.end(), std::mem_fn(&UICommon::GameFile::IsDatelDisc)); diff --git a/Source/Core/DolphinTool/CMakeLists.txt b/Source/Core/DolphinTool/CMakeLists.txt new file mode 100644 index 0000000000..19eb651273 --- /dev/null +++ b/Source/Core/DolphinTool/CMakeLists.txt @@ -0,0 +1,22 @@ +add_executable(dolphin-tool + ToolHeadlessPlatform.cpp + Command.h + ConvertCommand.cpp + ConvertCommand.h + VerifyCommand.cpp + VerifyCommand.h + ToolMain.cpp +) + +set_target_properties(dolphin-tool PROPERTIES OUTPUT_NAME dolphin-tool) + +target_link_libraries(dolphin-tool +PRIVATE + core + discio + videocommon + cpp-optparse +) + +set(CPACK_PACKAGE_EXECUTABLES ${CPACK_PACKAGE_EXECUTABLES} dolphin-tool) +install(TARGETS dolphin-tool RUNTIME DESTINATION ${bindir}) diff --git a/Source/Core/DolphinTool/Command.h b/Source/Core/DolphinTool/Command.h new file mode 100644 index 0000000000..1a769c66b5 --- /dev/null +++ b/Source/Core/DolphinTool/Command.h @@ -0,0 +1,19 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +namespace DolphinTool +{ +class Command +{ +public: + Command() {} + virtual ~Command() {} + virtual int Main(const std::vector& args) = 0; +}; + +} // namespace DolphinTool diff --git a/Source/Core/DolphinTool/ConvertCommand.cpp b/Source/Core/DolphinTool/ConvertCommand.cpp new file mode 100644 index 0000000000..ca270e9d12 --- /dev/null +++ b/Source/Core/DolphinTool/ConvertCommand.cpp @@ -0,0 +1,301 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "DolphinTool/ConvertCommand.h" + +#include + +namespace DolphinTool +{ +int ConvertCommand::Main(const std::vector& args) +{ + auto parser = std::make_unique(); + + parser->usage("usage: convert [options]... [FILE]..."); + + parser->add_option("-i", "--input") + .type("string") + .action("store") + .help("Path to disc image FILE.") + .metavar("FILE"); + + parser->add_option("-o", "--output") + .type("string") + .action("store") + .help("Path to the destination FILE.") + .metavar("FILE"); + + parser->add_option("-f", "--format") + .type("string") + .action("store") + .help("Container format to use. Default is RVZ. [%choices]") + .choices({"iso", "gcz", "wia", "rvz"}); + + parser->add_option("-s", "--scrub") + .action("store_true") + .help("Scrub junk data as part of conversion."); + + parser->add_option("-b", "--block_size") + .type("int") + .action("store") + .help("Block size for GCZ/WIA/RVZ formats, as an integer. Suggested value for RVZ: 131072 " + "(128 KiB)"); + + parser->add_option("-c", "--compression") + .type("string") + .action("store") + .help("Compression method to use when converting to WIA/RVZ. Suggested value for RVZ: zstd " + "[%choices]") + .choices({"none", "zstd", "bzip", "lzma", "lzma2"}); + + parser->add_option("-l", "--compression_level") + .type("int") + .action("store") + .help("Level of compression for the selected method. Ignored if 'none'. Suggested value for " + "zstd: 5"); + + const optparse::Values& options = parser->parse_args(args); + + // Validate options + + // --input + const std::string input_file_path = static_cast(options.get("input")); + if (input_file_path.empty()) + { + std::cerr << "Error: No input set" << std::endl; + return 1; + } + + // --output + const std::string output_file_path = static_cast(options.get("output")); + if (output_file_path.empty()) + { + std::cerr << "Error: No output set" << std::endl; + return 1; + } + + // --format + const std::optional format_o = + ParseFormatString(static_cast(options.get("format"))); + if (!format_o.has_value()) + { + std::cerr << "Error: No output format set" << std::endl; + return 1; + } + const DiscIO::BlobType format = format_o.value(); + + // Open the volume now for inspection + std::unique_ptr volume = DiscIO::CreateVolume(input_file_path); + if (!volume) + { + std::cerr << "Error: Unable to open disc image" << std::endl; + return 1; + } + + // --scrub + const bool scrub = static_cast(options.get("scrub")); + + if (scrub && volume->IsDatelDisc()) + { + std::cerr << "Error: Scrubbing a Datel disc is not supported"; + return 1; + } + + if (scrub && format == DiscIO::BlobType::RVZ) + { + std::cerr << "Warning: Scrubbing an RVZ container does not offer significant space advantages. " + "Continuing anyway." + << std::endl; + } + + if (scrub && format == DiscIO::BlobType::PLAIN) + { + std::cerr << "Warning: Scrubbing does not save space when converting to ISO unless using " + "external compression. Continuing anyway." + << std::endl; + } + + if (!scrub && format == DiscIO::BlobType::GCZ && + volume->GetVolumeType() == DiscIO::Platform::WiiDisc && !volume->IsDatelDisc()) + { + std::cerr << "Warning: Converting Wii disc images to GCZ without scrubbing may not offer space " + "advantages over ISO. Continuing anyway." + << std::endl; + } + + if (volume->IsNKit()) + { + std::cerr << "Warning: Converting an NKit file, output will still be NKit! Continuing anyway." + << std::endl; + } + + // --block_size + std::optional block_size_o; + if (options.is_set("block_size")) + block_size_o = static_cast(options.get("block_size")); + + if (format == DiscIO::BlobType::GCZ || format == DiscIO::BlobType::WIA || + format == DiscIO::BlobType::RVZ) + { + if (!block_size_o.has_value()) + { + std::cerr << "Error: Block size must be set for GCZ/RVZ/WIA" << std::endl; + return 1; + } + + if (!DiscIO::IsDiscImageBlockSizeValid(block_size_o.value(), format)) + { + std::cerr << "Error: Block size is not valid for this format" << std::endl; + return 1; + } + + if (block_size_o.value() < DiscIO::PREFERRED_MIN_BLOCK_SIZE || + block_size_o.value() > DiscIO::PREFERRED_MAX_BLOCK_SIZE) + { + std::cerr << "Warning: Block size is not ideal for performance. Continuing anyway." + << std::endl; + } + + if (format == DiscIO::BlobType::GCZ && + !DiscIO::IsGCZBlockSizeLegacyCompatible(block_size_o.value(), volume->GetSize())) + { + 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 " + "and must not be an integer multiple of the block size multiplied by 32. " + "Continuing anyway." + << std::endl; + } + } + + // --compress, --compress_level + std::optional compression_o = + ParseCompressionTypeString(static_cast(options.get("compression"))); + + std::optional compression_level_o; + if (options.is_set("compression_level")) + compression_level_o = static_cast(options.get("compression_level")); + + if (format == DiscIO::BlobType::WIA || format == DiscIO::BlobType::RVZ) + { + if (!compression_o.has_value()) + { + std::cerr << "Error: Compression format must be set for WIA or RVZ" << std::endl; + return 1; + } + + if ((format == DiscIO::BlobType::WIA && + compression_o.value() == DiscIO::WIARVZCompressionType::Zstd) || + (format == DiscIO::BlobType::RVZ && + compression_o.value() == DiscIO::WIARVZCompressionType::Purge)) + { + std::cerr << "Error: Compression type is not supported for the container format" << std::endl; + return 1; + } + + if (compression_o.value() == DiscIO::WIARVZCompressionType::None) + { + compression_level_o = 0; + } + else + { + if (!compression_level_o.has_value()) + { + std::cerr << "Error: Compression level must be set when compression type is not 'none'" + << std::endl; + return 1; + } + + const std::pair range = DiscIO::GetAllowedCompressionLevels(compression_o.value()); + if (compression_level_o.value() < range.first || compression_level_o.value() > range.second) + { + std::cerr << "Error: Compression level not in acceptable range" << std::endl; + return 1; + } + } + } + + // Open the blob reader + std::unique_ptr blob_reader = + scrub ? DiscIO::ScrubbedBlob::Create(input_file_path) : + DiscIO::CreateBlobReader(input_file_path); + if (!blob_reader) + { + std::cerr << "Error: Unable to process disc image. If --scrub is enabled, try again without it." + << std::endl; + return 1; + } + + // Perform the conversion + const auto NOOP_STATUS_CALLBACK = [](const std::string& text, float percent) { return true; }; + + bool success = false; + + switch (format) + { + case DiscIO::BlobType::PLAIN: + success = DiscIO::ConvertToPlain(blob_reader.get(), input_file_path, output_file_path, + NOOP_STATUS_CALLBACK); + break; + + case DiscIO::BlobType::GCZ: + success = DiscIO::ConvertToGCZ(blob_reader.get(), input_file_path, output_file_path, + volume->GetVolumeType() == DiscIO::Platform::WiiDisc ? 1 : 0, + block_size_o.value(), NOOP_STATUS_CALLBACK); + break; + + case DiscIO::BlobType::WIA: + case DiscIO::BlobType::RVZ: + success = DiscIO::ConvertToWIAOrRVZ(blob_reader.get(), input_file_path, output_file_path, + format == DiscIO::BlobType::RVZ, compression_o.value(), + compression_level_o.value(), block_size_o.value(), + NOOP_STATUS_CALLBACK); + break; + + default: + ASSERT(false); + break; + } + + if (!success) + { + std::cerr << "Error: Conversion failed" << std::endl; + return 1; + } + + return 0; +} + +std::optional +ConvertCommand::ParseCompressionTypeString(const std::string compression_str) +{ + if (compression_str == "none") + return DiscIO::WIARVZCompressionType::None; + else if (compression_str == "purge") + return DiscIO::WIARVZCompressionType::Purge; + else if (compression_str == "bzip2") + return DiscIO::WIARVZCompressionType::Bzip2; + else if (compression_str == "lzma") + return DiscIO::WIARVZCompressionType::LZMA; + else if (compression_str == "lzma2") + return DiscIO::WIARVZCompressionType::LZMA2; + else if (compression_str == "zstd") + return DiscIO::WIARVZCompressionType::Zstd; + else + return std::nullopt; +} + +std::optional ConvertCommand::ParseFormatString(const std::string format_str) +{ + if (format_str == "iso") + return DiscIO::BlobType::PLAIN; + else if (format_str == "gcz") + return DiscIO::BlobType::GCZ; + else if (format_str == "wia") + return DiscIO::BlobType::WIA; + else if (format_str == "rvz") + return DiscIO::BlobType::RVZ; + else + return std::nullopt; +} + +} // namespace DolphinTool diff --git a/Source/Core/DolphinTool/ConvertCommand.h b/Source/Core/DolphinTool/ConvertCommand.h new file mode 100644 index 0000000000..6776fd65f7 --- /dev/null +++ b/Source/Core/DolphinTool/ConvertCommand.h @@ -0,0 +1,30 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "DiscIO/Blob.h" +#include "DiscIO/DiscUtils.h" +#include "DiscIO/ScrubbedBlob.h" +#include "DiscIO/Volume.h" +#include "DiscIO/VolumeDisc.h" +#include "DiscIO/WIABlob.h" +#include "DolphinTool/Command.h" + +namespace DolphinTool +{ +class ConvertCommand final : public Command +{ +public: + int Main(const std::vector& args) override; + +private: + std::optional + ParseCompressionTypeString(const std::string compression_str); + std::optional ParseFormatString(const std::string format_str); +}; + +} // namespace DolphinTool diff --git a/Source/Core/DolphinTool/DolphinTool.exe.manifest b/Source/Core/DolphinTool/DolphinTool.exe.manifest new file mode 100644 index 0000000000..d3b615fb28 --- /dev/null +++ b/Source/Core/DolphinTool/DolphinTool.exe.manifest @@ -0,0 +1,30 @@ + + + +Dolphin Tool + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Source/Core/DolphinTool/DolphinTool.filters b/Source/Core/DolphinTool/DolphinTool.filters new file mode 100644 index 0000000000..aa59958652 --- /dev/null +++ b/Source/Core/DolphinTool/DolphinTool.filters @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Source/Core/DolphinTool/DolphinTool.rc b/Source/Core/DolphinTool/DolphinTool.rc new file mode 100644 index 0000000000..b1471b1760 --- /dev/null +++ b/Source/Core/DolphinTool/DolphinTool.rc @@ -0,0 +1,6 @@ +// Microsoft Visual C++ generated resource script. +// +#include "resource.h" +IDI_ICON1 ICON "..\\..\\..\\Installer\\Dolphin.ico" +"dolphin" ICON "..\\..\\..\\Installer\\Dolphin.ico" + diff --git a/Source/Core/DolphinTool/DolphinTool.vcxproj b/Source/Core/DolphinTool/DolphinTool.vcxproj new file mode 100644 index 0000000000..393438764e --- /dev/null +++ b/Source/Core/DolphinTool/DolphinTool.vcxproj @@ -0,0 +1,69 @@ + + + + + + {8F91523C-5C5E-4B22-A1F1-67560B6DC714} + + + + + + + + + + + + + + avrt.lib;iphlpapi.lib;winmm.lib;setupapi.lib;rpcrt4.lib;comctl32.lib;Shlwapi.lib;%(AdditionalDependencies) + opengl32.lib;avcodec.lib;avformat.lib;avutil.lib;swresample.lib;swscale.lib;%(AdditionalDependencies) + $(ExternalsDir)ffmpeg\lib;%(AdditionalLibraryDirectories) + Console + + + + + {D79392F7-06D6-4B4B-A39F-4D587C215D3A} + + + {41279555-f94f-4ebc-99de-af863c10c5c4} + + + {0e033be3-2e08-428e-9ae9-bc673efa12b5} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Source/Core/DolphinTool/ToolHeadlessPlatform.cpp b/Source/Core/DolphinTool/ToolHeadlessPlatform.cpp new file mode 100644 index 0000000000..7dc1cfaa60 --- /dev/null +++ b/Source/Core/DolphinTool/ToolHeadlessPlatform.cpp @@ -0,0 +1,85 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include + +#include +#include + +#include "Common/Flag.h" +#include "Common/WindowSystemInfo.h" + +#include "Core/Core.h" +#include "Core/DolphinAnalytics.h" +#include "Core/Host.h" + +// Begin stubs needed to satisfy Core dependencies +#include "VideoCommon/RenderBase.h" + +std::vector Host_GetPreferredLocales() +{ + return {}; +} + +void Host_NotifyMapLoaded() +{ +} + +void Host_RefreshDSPDebuggerWindow() +{ +} + +bool Host_UIBlocksControllerState() +{ + return false; +} + +void Host_Message(HostMessageID id) +{ +} + +void Host_UpdateTitle(const std::string& title) +{ +} + +void Host_UpdateDisasmDialog() +{ +} + +void Host_UpdateMainFrame() +{ +} + +void Host_RequestRenderWindowSize(int width, int height) +{ +} + +bool Host_RendererHasFocus() +{ + return false; +} + +bool Host_RendererHasFullFocus() +{ + return false; +} + +bool Host_RendererIsFullscreen() +{ + return false; +} + +void Host_YieldToUI() +{ +} + +void Host_TitleChanged() +{ +} + +std::unique_ptr Host_CreateGBAHost(std::weak_ptr core) +{ + return nullptr; +} +// End stubs to satisfy Core dependencies diff --git a/Source/Core/DolphinTool/ToolMain.cpp b/Source/Core/DolphinTool/ToolMain.cpp new file mode 100644 index 0000000000..31c668bd4a --- /dev/null +++ b/Source/Core/DolphinTool/ToolMain.cpp @@ -0,0 +1,45 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include + +#include "Common/Version.h" +#include "DolphinTool/Command.h" +#include "DolphinTool/ConvertCommand.h" +#include "DolphinTool/VerifyCommand.h" + +static int PrintUsage(int code) +{ + std::cerr << "usage: dolphin-tool COMMAND -h" << std::endl << std::endl; + std::cerr << "commands supported: [convert, verify]" << std::endl; + + return code; +} + +int main(int argc, char* argv[]) +{ + if (argc < 2) + return PrintUsage(1); + + std::vector args(argv, argv + argc); + + std::string command_str = args.at(1); + + // Take off the command selector before passing arguments down + args.erase(args.begin(), args.begin() + 1); + + std::unique_ptr command; + + if (command_str == "convert") + command = std::make_unique(); + else if (command_str == "verify") + command = std::make_unique(); + else + return PrintUsage(1); + + return command->Main(args); +} diff --git a/Source/Core/DolphinTool/VerifyCommand.cpp b/Source/Core/DolphinTool/VerifyCommand.cpp new file mode 100644 index 0000000000..6eb920a6f2 --- /dev/null +++ b/Source/Core/DolphinTool/VerifyCommand.cpp @@ -0,0 +1,175 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "DolphinTool/VerifyCommand.h" + +#include + +namespace DolphinTool +{ +int VerifyCommand::Main(const std::vector& args) +{ + auto parser = std::make_unique(); + + parser->usage("usage: verify [options]..."); + + parser->add_option("-i", "--input") + .type("string") + .action("store") + .help("Path to disc image FILE.") + .metavar("FILE"); + + parser->add_option("-a", "--algorithm") + .type("string") + .action("store") + .help("Optional. Compute and print the digest using the selected algorithm, then exit. " + "[%choices]") + .choices({"crc32", "md5", "sha1"}); + + const optparse::Values& options = parser->parse_args(args); + + // Validate options + const std::string input_file_path = static_cast(options.get("input")); + if (input_file_path.empty()) + { + std::cerr << "Error: No input set" << std::endl; + return 1; + } + + std::optional algorithm; + if (options.is_set("algorithm")) + { + algorithm = static_cast(options.get("algorithm")); + } + + bool enable_crc32 = algorithm == std::nullopt || algorithm == "crc32"; + bool enable_md5 = algorithm == std::nullopt || algorithm == "md5"; + bool enable_sha1 = algorithm == std::nullopt || algorithm == "sha1"; + + if (!enable_crc32 && !enable_md5 && !enable_sha1) + { + // optparse should protect from this + std::cerr << "Error: No algorithms selected for the operation" << std::endl; + return 1; + } + + // Open the volume + std::shared_ptr volume = DiscIO::CreateDisc(input_file_path); + if (!volume) + { + std::cerr << "Error: Unable to open disc image" << std::endl; + return 1; + } + + // Verify the volume + const std::optional result = + VerifyVolume(volume, enable_crc32, enable_md5, enable_sha1); + if (!result) + { + std::cerr << "Error: Unable to verify volume" << std::endl; + return 1; + } + + if (algorithm == std::nullopt) + { + PrintFullReport(result); + } + else + { + if (enable_crc32 && !result->hashes.crc32.empty()) + std::cout << HashToHexString(result->hashes.crc32) << std::endl; + else if (enable_md5 && !result->hashes.md5.empty()) + std::cout << HashToHexString(result->hashes.md5) << std::endl; + else if (enable_sha1 && !result->hashes.sha1.empty()) + std::cout << HashToHexString(result->hashes.sha1) << std::endl; + else + { + std::cerr << "Error: No hash available" << std::endl; + return 1; + } + } + + return 0; +} + +void VerifyCommand::PrintFullReport(const std::optional result) +{ + if (!result->hashes.crc32.empty()) + std::cout << "CRC32: " << HashToHexString(result->hashes.crc32) << std::endl; + else + std::cout << "CRC32 not available" << std::endl; + + if (!result->hashes.md5.empty()) + std::cout << "MD5: " << HashToHexString(result->hashes.md5) << std::endl; + else + std::cout << "MD5 not available" << std::endl; + + if (!result->hashes.sha1.empty()) + std::cout << "SHA1: " << HashToHexString(result->hashes.sha1) << std::endl; + else + std::cout << "SHA1 not available" << std::endl; + + std::cout << "Problems Found: " << (result->problems.size() > 0 ? "Yes" : "No") << std::endl; + + for (int i = 0; i < static_cast(result->problems.size()); ++i) + { + const DiscIO::VolumeVerifier::Problem problem = result->problems[i]; + + std::cout << std::endl << "Severity: "; + switch (problem.severity) + { + case DiscIO::VolumeVerifier::Severity::Low: + std::cout << "Low"; + break; + case DiscIO::VolumeVerifier::Severity::Medium: + std::cout << "Medium"; + break; + case DiscIO::VolumeVerifier::Severity::High: + std::cout << "High"; + break; + case DiscIO::VolumeVerifier::Severity::None: + std::cout << "None"; + break; + default: + ASSERT(false); + break; + } + std::cout << std::endl; + + std::cout << "Summary: " << problem.text << std::endl << std::endl; + } +} + +std::optional +VerifyCommand::VerifyVolume(std::shared_ptr volume, bool enable_crc32, + bool enable_md5, bool enable_sha1) +{ + if (!volume) + return std::nullopt; + + DiscIO::VolumeVerifier verifier(*volume, false, {enable_crc32, enable_md5, enable_sha1}); + + verifier.Start(); + while (verifier.GetBytesProcessed() != verifier.GetTotalBytes()) + { + verifier.Process(); + } + verifier.Finish(); + + const DiscIO::VolumeVerifier::Result result = verifier.GetResult(); + + return result; +} + +std::string VerifyCommand::HashToHexString(const std::vector& hash) +{ + std::stringstream ss; + ss << std::hex; + for (int i = 0; i < static_cast(hash.size()); ++i) + { + ss << std::setw(2) << std::setfill('0') << static_cast(hash[i]); + } + return ss.str(); +} + +} // namespace DolphinTool diff --git a/Source/Core/DolphinTool/VerifyCommand.h b/Source/Core/DolphinTool/VerifyCommand.h new file mode 100644 index 0000000000..1cd0794ba5 --- /dev/null +++ b/Source/Core/DolphinTool/VerifyCommand.h @@ -0,0 +1,32 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +#include "DiscIO/Volume.h" +#include "DiscIO/VolumeDisc.h" +#include "DiscIO/VolumeVerifier.h" +#include "DolphinTool/Command.h" + +namespace DolphinTool +{ +class VerifyCommand final : public Command +{ +public: + int Main(const std::vector& args) override; + +private: + void PrintFullReport(const std::optional result); + + std::optional + VerifyVolume(std::shared_ptr volume, bool enable_crc32, bool enable_md5, + bool enable_sha1); + + std::string HashToHexString(const std::vector& hash); +}; + +} // namespace DolphinTool diff --git a/Source/Core/DolphinTool/resource.h b/Source/Core/DolphinTool/resource.h new file mode 100644 index 0000000000..084246e52f --- /dev/null +++ b/Source/Core/DolphinTool/resource.h @@ -0,0 +1,23 @@ +// Copyright 2017 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by DolphinNoGui.rc +// +#ifdef RC_INVOKED +#define IDI_ICON1 101 +#else +#define IDI_ICON1 MAKEINTRESOURCE(101) +#endif + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/Source/dolphin-emu.sln b/Source/dolphin-emu.sln index 8e83448257..c61dea5263 100644 --- a/Source/dolphin-emu.sln +++ b/Source/dolphin-emu.sln @@ -7,6 +7,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dolphin", "Core\DolphinQt\D EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DolphinNoGUI", "Core\DolphinNoGUI\DolphinNoGUI.vcxproj", "{974E563D-23F8-4E8F-9083-F62876B04E08}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DolphinTool", "Core\DolphinTool\DolphinTool.vcxproj", "{8F91523C-5C5E-4B22-A1F1-67560B6DC714}" +EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DSPTool", "DSPTool\DSPTool.vcxproj", "{1970D175-3DE8-4738-942A-4D98D1CDBF64}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "UnitTests", "UnitTests\UnitTests.vcxproj", "{474661E7-C73A-43A6-AFEE-EE1EC433D49E}" @@ -95,6 +97,14 @@ Global {974E563D-23F8-4E8F-9083-F62876B04E08}.Debug|x64.ActiveCfg = Debug|x64 {974E563D-23F8-4E8F-9083-F62876B04E08}.Release|ARM64.ActiveCfg = Release|ARM64 {974E563D-23F8-4E8F-9083-F62876B04E08}.Release|x64.ActiveCfg = Release|x64 + {8F91523C-5C5E-4B22-A1F1-67560B6DC714}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {8F91523C-5C5E-4B22-A1F1-67560B6DC714}.Debug|ARM64.Build.0 = Debug|ARM64 + {8F91523C-5C5E-4B22-A1F1-67560B6DC714}.Debug|x64.ActiveCfg = Debug|x64 + {8F91523C-5C5E-4B22-A1F1-67560B6DC714}.Debug|x64.Build.0 = Debug|x64 + {8F91523C-5C5E-4B22-A1F1-67560B6DC714}.Release|ARM64.ActiveCfg = Release|ARM64 + {8F91523C-5C5E-4B22-A1F1-67560B6DC714}.Release|ARM64.Build.0 = Release|ARM64 + {8F91523C-5C5E-4B22-A1F1-67560B6DC714}.Release|x64.ActiveCfg = Release|x64 + {8F91523C-5C5E-4B22-A1F1-67560B6DC714}.Release|x64.Build.0 = Release|x64 {1970D175-3DE8-4738-942A-4D98D1CDBF64}.Debug|ARM64.ActiveCfg = Debug|ARM64 {1970D175-3DE8-4738-942A-4D98D1CDBF64}.Debug|ARM64.Build.0 = Debug|ARM64 {1970D175-3DE8-4738-942A-4D98D1CDBF64}.Debug|x64.ActiveCfg = Debug|x64