diff --git a/.gitignore b/.gitignore
index cebabd2c39..8e58ace16a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,8 @@ Thumbs.db
Externals/mGBA/version.c
Source/Core/Common/scmrev.h
# Ignore files output by build
+/cmake-build-debug
+/cmake-build-release
/[Bb]uild*/
/[Bb]inary*/
/obj/
diff --git a/Readme.md b/Readme.md
index 7fecf65cfd..3c9dc84730 100644
--- a/Readme.md
+++ b/Readme.md
@@ -193,7 +193,7 @@ is intended for debugging purposes only.
```
usage: dolphin-tool COMMAND -h
-commands supported: [convert, verify, header]
+commands supported: [convert, verify, header, extract]
```
```
@@ -252,3 +252,22 @@ then exit.
Optional. Print the level of compression for WIA/RVZ
formats, then exit.
```
+
+```
+Usage: extract [options]...
+
+Options:
+ -h, --help show this help message and exit
+ -i FILE, --input=FILE
+ Path to disc image FILE.
+ -o FOLDER, --output=FOLDER
+ Path to the destination FOLDER.
+ -p PARTITION, --partition=PARTITION
+ Which specific partition you want to extract.
+ -s SINGLE, --single=SINGLE
+ Which specific file/directory you want to extract.
+ -l, --list List all files in volume/partition. Will print the
+ directory/file specified with --single if defined.
+ -q, --quiet Mute all messages except for errors.
+ -g, --gameonly Only extracts the DATA partition.
+```
diff --git a/Source/Core/DolphinTool/CMakeLists.txt b/Source/Core/DolphinTool/CMakeLists.txt
index e2c8c8cdd1..e907fb9bbd 100644
--- a/Source/Core/DolphinTool/CMakeLists.txt
+++ b/Source/Core/DolphinTool/CMakeLists.txt
@@ -1,5 +1,7 @@
add_executable(dolphin-tool
ToolHeadlessPlatform.cpp
+ ExtractCommand.cpp
+ ExtractCommand.h
ConvertCommand.cpp
ConvertCommand.h
VerifyCommand.cpp
diff --git a/Source/Core/DolphinTool/DolphinTool.filters b/Source/Core/DolphinTool/DolphinTool.filters
index caa654146d..0ee07add9f 100644
--- a/Source/Core/DolphinTool/DolphinTool.filters
+++ b/Source/Core/DolphinTool/DolphinTool.filters
@@ -4,10 +4,12 @@
+
+
diff --git a/Source/Core/DolphinTool/DolphinTool.vcxproj b/Source/Core/DolphinTool/DolphinTool.vcxproj
index 9fcfd32020..8bfc2ee295 100644
--- a/Source/Core/DolphinTool/DolphinTool.vcxproj
+++ b/Source/Core/DolphinTool/DolphinTool.vcxproj
@@ -30,6 +30,7 @@
+
@@ -52,6 +53,7 @@
+
diff --git a/Source/Core/DolphinTool/ExtractCommand.cpp b/Source/Core/DolphinTool/ExtractCommand.cpp
new file mode 100644
index 0000000000..1d9d24b8d4
--- /dev/null
+++ b/Source/Core/DolphinTool/ExtractCommand.cpp
@@ -0,0 +1,361 @@
+// Copyright 2024 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "DolphinTool/ExtractCommand.h"
+
+#include
+#include
+#include
+
+#include
+#include
+
+#include
+
+#include "Common/FileUtil.h"
+
+#include "DiscIO/DiscExtractor.h"
+#include "DiscIO/DiscUtils.h"
+#include "DiscIO/Filesystem.h"
+#include "DiscIO/Volume.h"
+
+namespace DolphinTool
+{
+static void ExtractFile(const DiscIO::Volume& disc_volume, const DiscIO::Partition& partition,
+ const std::string& path, const std::string& out)
+{
+ const DiscIO::FileSystem* filesystem = disc_volume.GetFileSystem(partition);
+ if (!filesystem)
+ return;
+
+ ExportFile(disc_volume, partition, filesystem->FindFileInfo(path).get(), out);
+}
+
+static std::unique_ptr GetFileInfo(const DiscIO::Volume& disc_volume,
+ const DiscIO::Partition& partition,
+ const std::string& path)
+{
+ const DiscIO::FileSystem* filesystem = disc_volume.GetFileSystem(partition);
+ if (!filesystem)
+ return nullptr;
+
+ std::unique_ptr info = filesystem->FindFileInfo(path);
+ return info;
+}
+
+static bool VolumeSupported(const DiscIO::Volume& disc_volume)
+{
+ switch (disc_volume.GetVolumeType())
+ {
+ case DiscIO::Platform::WiiWAD:
+ fmt::println(std::cerr, "Error: Wii WADs are not supported.");
+ return false;
+ case DiscIO::Platform::ELFOrDOL:
+ fmt::println(std::cerr,
+ "Error: *.elf or *.dol have no filesystem and are therefore not supported.");
+ return false;
+ case DiscIO::Platform::WiiDisc:
+ case DiscIO::Platform::GameCubeDisc:
+ return true;
+ default:
+ fmt::println(std::cerr, "Error: Unknown volume type.");
+ return false;
+ }
+}
+
+static void ExtractDirectory(const DiscIO::Volume& disc_volume, const DiscIO::Partition& partition,
+ const std::string& path, const std::string& out, bool quiet)
+{
+ const DiscIO::FileSystem* filesystem = disc_volume.GetFileSystem(partition);
+ if (!filesystem)
+ return;
+
+ const std::unique_ptr info = filesystem->FindFileInfo(path);
+ u32 size = info->GetTotalChildren();
+ u32 files = 0;
+ ExportDirectory(
+ disc_volume, partition, *info, true, "", out,
+ [&files, &size, &quiet](const std::string& current) {
+ files++;
+ const float progress = static_cast(files) / static_cast(size) * 100;
+ if (!quiet)
+ fmt::println(std::cerr, "Extracting: {} | {}%", current, static_cast(progress));
+ return false;
+ });
+}
+
+static bool ExtractSystemData(const DiscIO::Volume& disc_volume, const DiscIO::Partition& partition,
+ const std::string& out)
+{
+ return ExportSystemData(disc_volume, partition, out);
+}
+
+static void ExtractPartition(const DiscIO::Volume& disc_volume, const DiscIO::Partition& partition,
+ const std::string& out, bool quiet)
+{
+ ExtractDirectory(disc_volume, partition, "", out + "/files", quiet);
+ ExtractSystemData(disc_volume, partition, out);
+}
+
+static bool ListPartition(const DiscIO::Volume& disc_volume, const DiscIO::Partition& partition,
+ const std::string& partition_name, const std::string& path,
+ std::string* result_text)
+{
+ const DiscIO::FileSystem* filesystem = disc_volume.GetFileSystem(partition);
+ const std::unique_ptr info = filesystem->FindFileInfo(path);
+
+ if (!info)
+ {
+ if (!partition_name.empty())
+ {
+ fmt::println(std::cerr, "Warning: {} does not exist in this partition.", path);
+ }
+ return false;
+ }
+
+ for (auto it = info->begin(); it != info->end(); ++it)
+ {
+ const std::string file_name = fmt::format("{}\n", it->GetName());
+ fmt::print(std::cout, "{}", file_name);
+ result_text->append(file_name);
+ }
+ return true;
+}
+
+static bool ListVolume(const DiscIO::Volume& disc_volume, const std::string& path,
+ const std::string& specific_partition_name, bool quiet,
+ std::string* result_text)
+{
+ if (disc_volume.GetPartitions().empty())
+ {
+ return ListPartition(disc_volume, DiscIO::PARTITION_NONE, specific_partition_name, path,
+ result_text);
+ }
+
+ bool success = false;
+ for (DiscIO::Partition& p : disc_volume.GetPartitions())
+ {
+ const std::optional partition_type = disc_volume.GetPartitionType(p);
+ if (!partition_type)
+ {
+ fmt::println(std::cerr, "Error: Could not get partition type.");
+ return false;
+ }
+ const std::string partition_name = DiscIO::NameForPartitionType(*partition_type, true);
+
+ if (!specific_partition_name.empty() &&
+ !Common::CaseInsensitiveEquals(partition_name, specific_partition_name))
+ {
+ continue;
+ }
+
+ const std::string partition_start =
+ fmt::format("/// PARTITION: {} <{}> ///\n", partition_name, path);
+ fmt::print(std::cout, "{}", partition_start);
+ result_text->append(partition_start);
+
+ success |= ListPartition(disc_volume, p, specific_partition_name, path, result_text);
+ }
+
+ return success;
+}
+
+static bool HandleExtractPartition(const std::string& output, const std::string& single_file_path,
+ const std::string& partition_name, DiscIO::Volume& disc_volume,
+ const DiscIO::Partition& partition, bool quiet, bool single)
+{
+ std::string file;
+ file.append(output).append("/");
+ file.append(partition_name).append("/");
+ if (!single)
+ {
+ ExtractPartition(disc_volume, partition, file, quiet);
+ return true;
+ }
+
+ if (auto file_info = GetFileInfo(disc_volume, partition, single_file_path); file_info != nullptr)
+ {
+ file.append("files/").append(single_file_path);
+ File::CreateFullPath(file);
+ if (file_info->IsDirectory())
+ {
+ file = PathToString(StringToPath(file).remove_filename());
+ ExtractDirectory(disc_volume, partition, single_file_path, file, quiet);
+ }
+ else
+ {
+ ExtractFile(disc_volume, partition, single_file_path, file);
+ }
+
+ return true;
+ }
+ return false;
+}
+
+int Extract(const std::vector& args)
+{
+ optparse::OptionParser parser;
+
+ parser.usage("usage: extract [options]...");
+
+ 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 FOLDER.")
+ .metavar("FOLDER");
+ parser.add_option("-p", "--partition")
+ .type("string")
+ .action("store")
+ .help("Which specific partition you want to extract.");
+ parser.add_option("-s", "--single")
+ .type("string")
+ .action("store")
+ .help("Which specific file/directory you want to extract.");
+ parser.add_option("-l", "--list")
+ .action("store_true")
+ .help("List all files in volume/partition. Will print the directory/file specified with "
+ "--single if defined.");
+ parser.add_option("-q", "--quiet")
+ .action("store_true")
+ .help("Mute all messages except for errors.");
+ parser.add_option("-g", "--gameonly")
+ .action("store_true")
+ .help("Only extracts the DATA partition.");
+
+ const optparse::Values& options = parser.parse_args(args);
+
+ const bool quiet = options.is_set("quiet");
+ const bool gameonly = options.is_set("gameonly");
+
+ if (!options.is_set("input"))
+ {
+ fmt::println(std::cerr, "Error: No input image set");
+ return EXIT_FAILURE;
+ }
+ const std::string& input_file_path = options["input"];
+
+ const std::string& output_folder_path = options["output"];
+
+ if (!options.is_set("output") && !options.is_set("list"))
+ {
+ fmt::println(std::cerr, "Error: No output folder set");
+ return EXIT_FAILURE;
+ }
+
+ const std::string& single_file_path = options["single"];
+ std::string specific_partition = options["partition"];
+
+ if (options.is_set("output") && !options.is_set("list"))
+ File::CreateDirs(output_folder_path);
+
+ if (gameonly)
+ specific_partition = std::string("data");
+
+ if (const std::unique_ptr blob_reader =
+ DiscIO::CreateBlobReader(input_file_path);
+ !blob_reader)
+ {
+ fmt::println(std::cerr, "Error: Unable to open disc image");
+ return EXIT_FAILURE;
+ }
+
+ const std::unique_ptr disc_volume = DiscIO::CreateVolume(input_file_path);
+
+ if (!disc_volume)
+ {
+ fmt::println(std::cerr, "Error: Unable to open volume");
+ return EXIT_FAILURE;
+ }
+
+ if (!VolumeSupported(*disc_volume))
+ return EXIT_FAILURE;
+
+ if (options.is_set("list"))
+ {
+ std::string list_path = options.is_set("single") ? single_file_path : "/";
+ if (quiet && !options.is_set("output"))
+ {
+ fmt::println(std::cerr, "Error: --quiet is set but no output file provided. Please either "
+ "remove the --quiet flag or specify --output");
+ return EXIT_FAILURE;
+ }
+
+ std::string text;
+ if (!ListVolume(*disc_volume, list_path, specific_partition, quiet, &text))
+ {
+ fmt::println(std::cerr, "Error: Found nothing to list");
+ return EXIT_FAILURE;
+ }
+
+ if (options.is_set("output"))
+ {
+ File::CreateFullPath(output_folder_path);
+ std::ofstream output_file;
+ output_file.open(output_folder_path);
+ if (!output_file.is_open())
+ {
+ fmt::println(std::cerr, "Error: Unable to open output file");
+ return EXIT_FAILURE;
+ }
+ output_file << text;
+ }
+
+ return EXIT_SUCCESS;
+ }
+
+ bool extracted_one = false;
+
+ if (disc_volume->GetPartitions().empty())
+ {
+ if (options.is_set("partition"))
+ {
+ fmt::println(
+ std::cerr,
+ "Warning: --partition has a value even though this image doesn't have any partitions.");
+ }
+
+ extracted_one = HandleExtractPartition(output_folder_path, single_file_path, "", *disc_volume,
+ DiscIO::PARTITION_NONE, quiet, options.is_set("single"));
+ }
+ else
+ {
+ for (DiscIO::Partition& p : disc_volume->GetPartitions())
+ {
+ if (const std::optional partition_type = disc_volume->GetPartitionType(p))
+ {
+ const std::string partition_name = DiscIO::NameForPartitionType(*partition_type, true);
+
+ if (!specific_partition.empty() &&
+ !Common::CaseInsensitiveEquals(specific_partition, partition_name))
+ {
+ continue;
+ }
+
+ extracted_one |=
+ HandleExtractPartition(output_folder_path, single_file_path, partition_name,
+ *disc_volume, p, quiet, options.is_set("single"));
+ }
+ }
+ }
+
+ if (!extracted_one)
+ {
+ if (options.is_set("single"))
+ fmt::print(std::cerr, "Error: No file/folder was extracted.");
+ else
+ fmt::print(std::cerr, "Error: No partitions were extracted.");
+ if (options.is_set("partition"))
+ fmt::println(std::cerr, " Maybe you misspelled your specified partition?");
+ fmt::println(std::cerr, "\n");
+ return EXIT_FAILURE;
+ }
+
+ fmt::println(std::cerr, "Finished Successfully!");
+ return EXIT_SUCCESS;
+}
+} // namespace DolphinTool
diff --git a/Source/Core/DolphinTool/ExtractCommand.h b/Source/Core/DolphinTool/ExtractCommand.h
new file mode 100644
index 0000000000..c69ffdf386
--- /dev/null
+++ b/Source/Core/DolphinTool/ExtractCommand.h
@@ -0,0 +1,12 @@
+// Copyright 2024 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include
+#include
+
+namespace DolphinTool
+{
+int Extract(const std::vector& args);
+} // namespace DolphinTool
diff --git a/Source/Core/DolphinTool/ToolMain.cpp b/Source/Core/DolphinTool/ToolMain.cpp
index 63bfdb6342..224ead3d7b 100644
--- a/Source/Core/DolphinTool/ToolMain.cpp
+++ b/Source/Core/DolphinTool/ToolMain.cpp
@@ -3,7 +3,6 @@
#include
#include
-#include
#include
#include
#include
@@ -13,10 +12,10 @@
#include
#include "Common/StringUtil.h"
-#include "Common/Version.h"
#include "Core/Core.h"
#include "DolphinTool/ConvertCommand.h"
+#include "DolphinTool/ExtractCommand.h"
#include "DolphinTool/HeaderCommand.h"
#include "DolphinTool/VerifyCommand.h"
@@ -24,7 +23,7 @@ static void PrintUsage()
{
fmt::print(std::cerr, "usage: dolphin-tool COMMAND -h\n"
"\n"
- "commands supported: [convert, verify, header]\n");
+ "commands supported: [convert, verify, header, extract]\n");
}
#ifdef _WIN32
@@ -51,6 +50,8 @@ int main(int argc, char* argv[])
return DolphinTool::VerifyCommand(args);
else if (command_str == "header")
return DolphinTool::HeaderCommand(args);
+ else if (command_str == "extract")
+ return DolphinTool::Extract(args);
PrintUsage();
return EXIT_FAILURE;
}