ConvertCommand.cpp 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. // Copyright 2021 Dolphin Emulator Project
  2. // SPDX-License-Identifier: GPL-2.0-or-later
  3. #include "DolphinTool/ConvertCommand.h"
  4. #include <cstdlib>
  5. #include <iostream>
  6. #include <limits>
  7. #include <optional>
  8. #include <string>
  9. #include <vector>
  10. #include <OptionParser.h>
  11. #include <fmt/format.h>
  12. #include <fmt/ostream.h>
  13. #include "Common/CommonTypes.h"
  14. #include "DiscIO/Blob.h"
  15. #include "DiscIO/DiscUtils.h"
  16. #include "DiscIO/ScrubbedBlob.h"
  17. #include "DiscIO/Volume.h"
  18. #include "DiscIO/VolumeDisc.h"
  19. #include "DiscIO/WIABlob.h"
  20. #include "UICommon/UICommon.h"
  21. namespace DolphinTool
  22. {
  23. static std::optional<DiscIO::WIARVZCompressionType>
  24. ParseCompressionTypeString(const std::string& compression_str)
  25. {
  26. if (compression_str == "none")
  27. return DiscIO::WIARVZCompressionType::None;
  28. else if (compression_str == "purge")
  29. return DiscIO::WIARVZCompressionType::Purge;
  30. else if (compression_str == "bzip2")
  31. return DiscIO::WIARVZCompressionType::Bzip2;
  32. else if (compression_str == "lzma")
  33. return DiscIO::WIARVZCompressionType::LZMA;
  34. else if (compression_str == "lzma2")
  35. return DiscIO::WIARVZCompressionType::LZMA2;
  36. else if (compression_str == "zstd")
  37. return DiscIO::WIARVZCompressionType::Zstd;
  38. return std::nullopt;
  39. }
  40. static std::optional<DiscIO::BlobType> ParseFormatString(const std::string& format_str)
  41. {
  42. if (format_str == "iso")
  43. return DiscIO::BlobType::PLAIN;
  44. else if (format_str == "gcz")
  45. return DiscIO::BlobType::GCZ;
  46. else if (format_str == "wia")
  47. return DiscIO::BlobType::WIA;
  48. else if (format_str == "rvz")
  49. return DiscIO::BlobType::RVZ;
  50. return std::nullopt;
  51. }
  52. int ConvertCommand(const std::vector<std::string>& args)
  53. {
  54. optparse::OptionParser parser;
  55. parser.usage("usage: convert [options]... [FILE]...");
  56. parser.add_option("-u", "--user")
  57. .type("string")
  58. .action("store")
  59. .help("User folder path, required for temporary processing files. "
  60. "Will be automatically created if this option is not set.")
  61. .set_default("");
  62. parser.add_option("-i", "--input")
  63. .type("string")
  64. .action("store")
  65. .help("Path to disc image FILE.")
  66. .metavar("FILE");
  67. parser.add_option("-o", "--output")
  68. .type("string")
  69. .action("store")
  70. .help("Path to the destination FILE.")
  71. .metavar("FILE");
  72. parser.add_option("-f", "--format")
  73. .type("string")
  74. .action("store")
  75. .help("Container format to use. Default is RVZ. [%choices]")
  76. .choices({"iso", "gcz", "wia", "rvz"});
  77. parser.add_option("-s", "--scrub")
  78. .action("store_true")
  79. .help("Scrub junk data as part of conversion.");
  80. parser.add_option("-b", "--block_size")
  81. .type("int")
  82. .action("store")
  83. .help("Block size for GCZ/WIA/RVZ formats, as an integer. Suggested value for RVZ: 131072 "
  84. "(128 KiB)");
  85. parser.add_option("-c", "--compression")
  86. .type("string")
  87. .action("store")
  88. .help("Compression method to use when converting to WIA/RVZ. Suggested value for RVZ: zstd "
  89. "[%choices]")
  90. .choices({"none", "zstd", "bzip2", "lzma", "lzma2"});
  91. parser.add_option("-l", "--compression_level")
  92. .type("int")
  93. .action("store")
  94. .help("Level of compression for the selected method. Ignored if 'none'. Suggested value for "
  95. "zstd: 5");
  96. const optparse::Values& options = parser.parse_args(args);
  97. // Initialize the dolphin user directory, required for temporary processing files
  98. // If this is not set, destructive file operations could occur due to path confusion
  99. UICommon::SetUserDirectory(options["user"]);
  100. UICommon::Init();
  101. // Validate options
  102. // --input
  103. if (!options.is_set("input"))
  104. {
  105. fmt::print(std::cerr, "Error: No input set\n");
  106. return EXIT_FAILURE;
  107. }
  108. const std::string& input_file_path = options["input"];
  109. // --output
  110. if (!options.is_set("output"))
  111. {
  112. fmt::print(std::cerr, "Error: No output set\n");
  113. return EXIT_FAILURE;
  114. }
  115. const std::string& output_file_path = options["output"];
  116. // --format
  117. const std::optional<DiscIO::BlobType> format_o = ParseFormatString(options["format"]);
  118. if (!format_o.has_value())
  119. {
  120. fmt::print(std::cerr, "Error: No output format set\n");
  121. return EXIT_FAILURE;
  122. }
  123. const DiscIO::BlobType format = format_o.value();
  124. // Open the blob reader
  125. std::unique_ptr<DiscIO::BlobReader> blob_reader = DiscIO::CreateBlobReader(input_file_path);
  126. if (!blob_reader)
  127. {
  128. fmt::print(std::cerr, "Error: The input file could not be opened.\n");
  129. return EXIT_FAILURE;
  130. }
  131. // --scrub
  132. const bool scrub = static_cast<bool>(options.get("scrub"));
  133. // Open the volume
  134. std::unique_ptr<DiscIO::Volume> volume = DiscIO::CreateDisc(input_file_path);
  135. if (!volume)
  136. {
  137. if (scrub)
  138. {
  139. fmt::print(std::cerr, "Error: Scrubbing is only supported for GC/Wii disc images.\n");
  140. return EXIT_FAILURE;
  141. }
  142. fmt::print(std::cerr,
  143. "Warning: The input file is not a GC/Wii disc image. Continuing anyway.\n");
  144. }
  145. if (scrub)
  146. {
  147. if (volume->IsDatelDisc())
  148. {
  149. fmt::print(std::cerr, "Error: Scrubbing a Datel disc is not supported.\n");
  150. return EXIT_FAILURE;
  151. }
  152. blob_reader = DiscIO::ScrubbedBlob::Create(input_file_path);
  153. if (!blob_reader)
  154. {
  155. fmt::print(std::cerr, "Error: Unable to process disc image. Try again without --scrub.\n");
  156. return EXIT_FAILURE;
  157. }
  158. }
  159. if (scrub && format == DiscIO::BlobType::RVZ)
  160. {
  161. fmt::print(std::cerr, "Warning: Scrubbing an RVZ container does not offer significant space "
  162. "advantages. Continuing anyway.\n");
  163. }
  164. if (scrub && format == DiscIO::BlobType::PLAIN)
  165. {
  166. fmt::print(std::cerr, "Warning: Scrubbing does not save space when converting to ISO unless "
  167. "using external compression. Continuing anyway.\n");
  168. }
  169. if (!scrub && format == DiscIO::BlobType::GCZ && volume &&
  170. volume->GetVolumeType() == DiscIO::Platform::WiiDisc && !volume->IsDatelDisc())
  171. {
  172. fmt::print(std::cerr, "Warning: Converting Wii disc images to GCZ without scrubbing may not "
  173. "offer space advantages over ISO. Continuing anyway.\n");
  174. }
  175. if (volume && volume->IsNKit())
  176. {
  177. fmt::print(std::cerr,
  178. "Warning: Converting an NKit file, output will still be NKit! Continuing anyway.\n");
  179. }
  180. // --block_size
  181. std::optional<int> block_size_o;
  182. if (options.is_set("block_size"))
  183. block_size_o = static_cast<int>(options.get("block_size"));
  184. if (format == DiscIO::BlobType::GCZ || format == DiscIO::BlobType::WIA ||
  185. format == DiscIO::BlobType::RVZ)
  186. {
  187. if (!block_size_o.has_value())
  188. {
  189. fmt::print(std::cerr, "Error: Block size must be set for GCZ/RVZ/WIA\n");
  190. return EXIT_FAILURE;
  191. }
  192. if (!DiscIO::IsDiscImageBlockSizeValid(block_size_o.value(), format))
  193. {
  194. fmt::print(std::cerr, "Error: Block size is not valid for this format\n");
  195. return EXIT_FAILURE;
  196. }
  197. if (block_size_o.value() < DiscIO::PREFERRED_MIN_BLOCK_SIZE ||
  198. block_size_o.value() > DiscIO::PREFERRED_MAX_BLOCK_SIZE)
  199. {
  200. fmt::print(std::cerr,
  201. "Warning: Block size is not ideal for performance. Continuing anyway.\n");
  202. }
  203. if (format == DiscIO::BlobType::GCZ && volume &&
  204. !DiscIO::IsGCZBlockSizeLegacyCompatible(block_size_o.value(), volume->GetDataSize()))
  205. {
  206. fmt::print(std::cerr,
  207. "Warning: For GCZs to be compatible with Dolphin < 5.0-11893, the file size "
  208. "must be an integer multiple of the block size and must not be an integer "
  209. "multiple of the block size multiplied by 32. Continuing anyway.\n");
  210. }
  211. }
  212. // --compress, --compress_level
  213. std::optional<DiscIO::WIARVZCompressionType> compression_o =
  214. ParseCompressionTypeString(options["compression"]);
  215. std::optional<int> compression_level_o;
  216. if (options.is_set("compression_level"))
  217. compression_level_o = static_cast<int>(options.get("compression_level"));
  218. if (format == DiscIO::BlobType::WIA || format == DiscIO::BlobType::RVZ)
  219. {
  220. if (!compression_o.has_value())
  221. {
  222. fmt::print(std::cerr, "Error: Compression method must be set for WIA or RVZ\n");
  223. return EXIT_FAILURE;
  224. }
  225. if ((format == DiscIO::BlobType::WIA &&
  226. compression_o.value() == DiscIO::WIARVZCompressionType::Zstd) ||
  227. (format == DiscIO::BlobType::RVZ &&
  228. compression_o.value() == DiscIO::WIARVZCompressionType::Purge))
  229. {
  230. fmt::print(std::cerr, "Error: Compression type is not supported for the container format\n");
  231. return EXIT_FAILURE;
  232. }
  233. if (compression_o.value() == DiscIO::WIARVZCompressionType::None)
  234. {
  235. compression_level_o = 0;
  236. }
  237. else
  238. {
  239. if (!compression_level_o.has_value())
  240. {
  241. fmt::print(std::cerr,
  242. "Error: Compression level must be set when compression type is not 'none'\n");
  243. return EXIT_FAILURE;
  244. }
  245. const std::pair<int, int> range =
  246. DiscIO::GetAllowedCompressionLevels(compression_o.value(), false);
  247. if (compression_level_o.value() < range.first || compression_level_o.value() > range.second)
  248. {
  249. fmt::print(std::cerr, "Error: Compression level not in acceptable range\n");
  250. return EXIT_FAILURE;
  251. }
  252. }
  253. }
  254. // Perform the conversion
  255. const auto NOOP_STATUS_CALLBACK = [](const std::string& text, float percent) { return true; };
  256. bool success = false;
  257. switch (format)
  258. {
  259. case DiscIO::BlobType::PLAIN:
  260. {
  261. success = DiscIO::ConvertToPlain(blob_reader.get(), input_file_path, output_file_path,
  262. NOOP_STATUS_CALLBACK);
  263. break;
  264. }
  265. case DiscIO::BlobType::GCZ:
  266. {
  267. u32 sub_type = std::numeric_limits<u32>::max();
  268. if (volume)
  269. {
  270. if (volume->GetVolumeType() == DiscIO::Platform::GameCubeDisc)
  271. sub_type = 0;
  272. else if (volume->GetVolumeType() == DiscIO::Platform::WiiDisc)
  273. sub_type = 1;
  274. }
  275. success = DiscIO::ConvertToGCZ(blob_reader.get(), input_file_path, output_file_path, sub_type,
  276. block_size_o.value(), NOOP_STATUS_CALLBACK);
  277. break;
  278. }
  279. case DiscIO::BlobType::WIA:
  280. case DiscIO::BlobType::RVZ:
  281. {
  282. success = DiscIO::ConvertToWIAOrRVZ(blob_reader.get(), input_file_path, output_file_path,
  283. format == DiscIO::BlobType::RVZ, compression_o.value(),
  284. compression_level_o.value(), block_size_o.value(),
  285. NOOP_STATUS_CALLBACK);
  286. break;
  287. }
  288. default:
  289. {
  290. ASSERT(false);
  291. break;
  292. }
  293. }
  294. if (!success)
  295. {
  296. fmt::print(std::cerr, "Error: Conversion failed\n");
  297. return EXIT_FAILURE;
  298. }
  299. return EXIT_SUCCESS;
  300. }
  301. } // namespace DolphinTool