301 changes: 301 additions & 0 deletions 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 <OptionParser.h>

namespace DolphinTool
{
int ConvertCommand::Main(const std::vector<std::string>& args)
{
auto parser = std::make_unique<optparse::OptionParser>();

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<const char*>(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<const char*>(options.get("output"));
if (output_file_path.empty())
{
std::cerr << "Error: No output set" << std::endl;
return 1;
}

// --format
const std::optional<DiscIO::BlobType> format_o =
ParseFormatString(static_cast<const char*>(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<DiscIO::Volume> 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<bool>(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<int> block_size_o;
if (options.is_set("block_size"))
block_size_o = static_cast<int>(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<DiscIO::WIARVZCompressionType> compression_o =
ParseCompressionTypeString(static_cast<const char*>(options.get("compression")));

std::optional<int> compression_level_o;
if (options.is_set("compression_level"))
compression_level_o = static_cast<int>(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<int, int> 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<DiscIO::BlobReader> 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<DiscIO::WIARVZCompressionType>
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<DiscIO::BlobType> 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
30 changes: 30 additions & 0 deletions 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 <cstdint>
#include <optional>

#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<std::string>& args) override;

private:
std::optional<DiscIO::WIARVZCompressionType>
ParseCompressionTypeString(const std::string compression_str);
std::optional<DiscIO::BlobType> ParseFormatString(const std::string format_str);
};

} // namespace DolphinTool
30 changes: 30 additions & 0 deletions Source/Core/DolphinTool/DolphinTool.exe.manifest
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
version="1.0.0.0"
processorArchitecture="amd64"
name="DolphinTeam.DolphinTool"
type="win32"
/>
<description>Dolphin Tool</description>
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="*"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
</assembly>
23 changes: 23 additions & 0 deletions Source/Core/DolphinTool/DolphinTool.filters
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<ClCompile Include="ConvertCommand.cpp" />
<ClCompile Include="VerifyCommand.cpp" />
<ClCompile Include="ToolHeadlessPlatform.cpp" />
<ClCompile Include="ToolMain.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="Command.h" />
<ClInclude Include="ConvertCommand.h" />
<ClInclude Include="VerifyCommand.h" />
</ItemGroup>
<ItemGroup>
<Manifest Include="DolphinTool.exe.manifest" />
</ItemGroup>
<ItemGroup>
<Text Include="CMakeLists.txt" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="DolphinTool.rc" />
</ItemGroup>
</Project>
6 changes: 6 additions & 0 deletions 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"

69 changes: 69 additions & 0 deletions Source/Core/DolphinTool/DolphinTool.vcxproj
@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\VSProps\Base.Macros.props" />
<Import Project="$(VSPropsDir)Base.Targets.props" />
<PropertyGroup Label="Globals">
<ProjectGuid>{8F91523C-5C5E-4B22-A1F1-67560B6DC714}</ProjectGuid>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<Import Project="$(VSPropsDir)Configuration.Application.props" />
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings" />
<ImportGroup Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="$(VSPropsDir)Base.props" />
<Import Project="$(VSPropsDir)PCHUse.props" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<ItemDefinitionGroup>
<Link>
<AdditionalDependencies>avrt.lib;iphlpapi.lib;winmm.lib;setupapi.lib;rpcrt4.lib;comctl32.lib;Shlwapi.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies Condition="'$(Platform)'=='x64'">opengl32.lib;avcodec.lib;avformat.lib;avutil.lib;swresample.lib;swscale.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalLibraryDirectories Condition="'$(Platform)'=='x64'">$(ExternalsDir)ffmpeg\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<SubSystem>Console</SubSystem>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ProjectReference Include="$(CoreDir)DolphinLib.vcxproj">
<Project>{D79392F7-06D6-4B4B-A39F-4D587C215D3A}</Project>
</ProjectReference>
<ProjectReference Include="$(CoreDir)Common\SCMRevGen.vcxproj">
<Project>{41279555-f94f-4ebc-99de-af863c10c5c4}</Project>
</ProjectReference>
<ProjectReference Include="$(DolphinRootDir)Languages\Languages.vcxproj">
<Project>{0e033be3-2e08-428e-9ae9-bc673efa12b5}</Project>
</ProjectReference>
</ItemGroup>
<Import Project="$(ExternalsDir)ExternalsReferenceAll.props" />
<ItemGroup>
<ClCompile Include="ConvertCommand.cpp" />
<ClCompile Include="VerifyCommand.cpp" />
<ClCompile Include="ToolHeadlessPlatform.cpp" />
<ClCompile Include="ToolMain.cpp" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
<!--Copy the .exe to binary output folder-->
<ItemGroup>
<SourceFiles Include="$(TargetPath)" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="Command.h" />
<ClInclude Include="ConvertCommand.h" />
<ClInclude Include="VerifyCommand.h" />
</ItemGroup>
<ItemGroup>
<Manifest Include="DolphinTool.exe.manifest" />
</ItemGroup>
<ItemGroup>
<Text Include="CMakeLists.txt" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="DolphinTool.rc" />
</ItemGroup>
<Target Name="AfterBuild" Inputs="@(SourceFiles)" Outputs="@(SourceFiles -> '$(BinaryOutputDir)%(Filename)%(Extension)')">
<Message Text="Copy: @(SourceFiles) -&gt; $(BinaryOutputDir)" Importance="High" />
<Copy SourceFiles="@(SourceFiles)" DestinationFolder="$(BinaryOutputDir)" />
</Target>
</Project>
85 changes: 85 additions & 0 deletions Source/Core/DolphinTool/ToolHeadlessPlatform.cpp
@@ -0,0 +1,85 @@
// Copyright 2021 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#include <cstdio>
#include <thread>

#include <memory>
#include <string>

#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<std::string> 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<GBAHostInterface> Host_CreateGBAHost(std::weak_ptr<HW::GBA::Core> core)
{
return nullptr;
}
// End stubs to satisfy Core dependencies
45 changes: 45 additions & 0 deletions Source/Core/DolphinTool/ToolMain.cpp
@@ -0,0 +1,45 @@
// Copyright 2021 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
#include <vector>

#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<std::string> 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<DolphinTool::Command> command;

if (command_str == "convert")
command = std::make_unique<DolphinTool::ConvertCommand>();
else if (command_str == "verify")
command = std::make_unique<DolphinTool::VerifyCommand>();
else
return PrintUsage(1);

return command->Main(args);
}
175 changes: 175 additions & 0 deletions 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 <OptionParser.h>

namespace DolphinTool
{
int VerifyCommand::Main(const std::vector<std::string>& args)
{
auto parser = std::make_unique<optparse::OptionParser>();

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<const char*>(options.get("input"));
if (input_file_path.empty())
{
std::cerr << "Error: No input set" << std::endl;
return 1;
}

std::optional<std::string> algorithm;
if (options.is_set("algorithm"))
{
algorithm = static_cast<const char*>(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<DiscIO::VolumeDisc> 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<DiscIO::VolumeVerifier::Result> 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<DiscIO::VolumeVerifier::Result> 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<int>(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<DiscIO::VolumeVerifier::Result>
VerifyCommand::VerifyVolume(std::shared_ptr<DiscIO::VolumeDisc> 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<u8>& hash)
{
std::stringstream ss;
ss << std::hex;
for (int i = 0; i < static_cast<int>(hash.size()); ++i)
{
ss << std::setw(2) << std::setfill('0') << static_cast<int>(hash[i]);
}
return ss.str();
}

} // namespace DolphinTool
32 changes: 32 additions & 0 deletions 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 <iostream>
#include <string>
#include <vector>

#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<std::string>& args) override;

private:
void PrintFullReport(const std::optional<DiscIO::VolumeVerifier::Result> result);

std::optional<DiscIO::VolumeVerifier::Result>
VerifyVolume(std::shared_ptr<DiscIO::VolumeDisc> volume, bool enable_crc32, bool enable_md5,
bool enable_sha1);

std::string HashToHexString(const std::vector<u8>& hash);
};

} // namespace DolphinTool
23 changes: 23 additions & 0 deletions 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
10 changes: 10 additions & 0 deletions Source/dolphin-emu.sln
Expand Up @@ -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}"
Expand Down Expand Up @@ -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
Expand Down