diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 15799c0c4..329af28d2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -203,6 +203,7 @@ if (ENABLE_QT) endif() if (ENABLE_QT) # Or any other hypothetical future frontends + add_subdirectory(citra_cli) add_subdirectory(citra_meta) endif() diff --git a/src/citra_cli/CMakeLists.txt b/src/citra_cli/CMakeLists.txt new file mode 100644 index 000000000..5b93f8d55 --- /dev/null +++ b/src/citra_cli/CMakeLists.txt @@ -0,0 +1,12 @@ +add_library(citra_cli STATIC EXCLUDE_FROM_ALL + citra_cli.h + citra_cli.cpp + compression_cli.h + compression_cli.cpp +) + +target_link_libraries(citra_cli PRIVATE citra_common citra_core) + +if (MSVC) + target_link_libraries(citra_cli PRIVATE getopt) +endif() diff --git a/src/citra_cli/citra_cli.cpp b/src/citra_cli/citra_cli.cpp new file mode 100644 index 000000000..791f1656f --- /dev/null +++ b/src/citra_cli/citra_cli.cpp @@ -0,0 +1,45 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#undef _UNICODE +#include +#ifndef _MSC_VER +#include +#endif + +#include "citra_cli/citra_cli.h" +#include "citra_cli/compression_cli.h" + +namespace CitraCLI { + +bool CheckForOptions(const char* optstring, int argc, char* argv[]) { + const int original_opterr = opterr; + opterr = 0; // Temporarily suppress invalid option messages + + bool return_value = false; + int option; + while ((option = getopt(argc, argv, optstring)) != -1) { + for (size_t i = 0; optstring[i] != '\0'; ++i) { + if (optstring[i] == ':') { + continue; + } + if (option == optstring[i]) { + return_value = true; + break; + } + } + } + + opterr = original_opterr; + optind = 1; // Reset getopt so that it can be used again + return return_value; +} + +int ParseCommand(int argc, char* argv[]) { + if (CheckForOptions(compression_ops_optstring, argc, argv)) { + return ParseCompressionCommand(argc, argv); + } +} + +} // namespace CitraCLI diff --git a/src/citra_cli/citra_cli.h b/src/citra_cli/citra_cli.h new file mode 100644 index 000000000..32082fab5 --- /dev/null +++ b/src/citra_cli/citra_cli.h @@ -0,0 +1,13 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +namespace CitraCLI { + +constexpr char compression_ops_optstring[] = "c:x:o:"; +constexpr char cli_capture_optstring[] = "c:x:o:"; + +bool CheckForOptions(const char* optstring, int argc, char* argv[]); +int ParseCommand(int argc, char* argv[]); + +} // namespace CitraCLI diff --git a/src/citra_cli/compression_cli.cpp b/src/citra_cli/compression_cli.cpp new file mode 100644 index 000000000..4ebce18b7 --- /dev/null +++ b/src/citra_cli/compression_cli.cpp @@ -0,0 +1,129 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#undef _UNICODE +#include +#ifndef _MSC_VER +#include +#endif + +#include "citra_cli/citra_cli.h" +#include "common/common_paths.h" +#include "common/logging/log.h" +#include "common/zstd_compression.h" +#include "core/loader/loader.h" + +namespace CitraCLI { + +static std::string strip_path_filename(std::string path) { + namespace fs = std::filesystem; + fs::path path_path = path; + fs::path stripped_path = path_path.remove_filename(); + return stripped_path.string(); +} + +static std::string build_output_path(std::string source_path, std::string extension, + std::string output_dir_path) { + namespace fs = std::filesystem; + fs::path source_path_path = source_path; + std::string recommended_filename = + source_path_path.filename().replace_extension(extension).string(); + return output_dir_path + DIR_SEP + recommended_filename; +} + +static bool perform_z3ds_operation(bool is_compressing, const std::string& src_file, + const std::string& dst_file, + const std::array& underlying_magic, size_t frame_size, + std::function&& update_callback, + std::unordered_map> metadata) { + if (is_compressing) { + return FileUtil::CompressZ3DSFile(src_file, dst_file, underlying_magic, frame_size, + std::move(update_callback), metadata); + } else { // decompressing + return FileUtil::DeCompressZ3DSFile(src_file, dst_file, std::move(update_callback)); + } +} + +int ParseCompressionCommand(int argc, char* argv[]) { + Common::Log::Initialize(); + Common::Log::Start(); + + const std::string common_error_addendum = "\nCheck log for more details."; + + std::optional compress_path; // The path of a decompressed file to be compressed + std::optional decompress_path; // The path of a compressed file to be decompressed + std::optional output_dir_path; // The directory which will contain processed file + + int option; + while ((option = getopt(argc, argv, compression_ops_optstring)) != -1) { + switch (option) { + case 'c': + compress_path = optarg; + break; + case 'x': + decompress_path = optarg; + break; + case 'o': + output_dir_path = optarg; + break; + } + } + + bool is_compressing; // True if compressing, false if decompressing + std::string source_path; + std::string action_description; // String containing a user-friendly verb + // describing the performed operation + if (compress_path.has_value()) { + is_compressing = true; + source_path = compress_path.value(); + action_description = "Compressing"; + } else if (decompress_path.has_value()) { + is_compressing = false; + source_path = decompress_path.value(); + action_description = "Decompressing"; + } else { + std::cout << "Invalid option combination provided. Quitting." << std::endl; + return 1; + } + + std::cout << action_description << " file '" << source_path << "'..." << std::flush; + + if (!output_dir_path.has_value()) { + output_dir_path = strip_path_filename(source_path); + } + + auto compress_info = Loader::GetCompressFileInfo(source_path, is_compressing); + + if (!compress_info.has_value()) { + std::cout << "fail: Failed to get compress info for file." << common_error_addendum + << std::endl; + return 1; + } + + std::string extension; // The extension that the final processed file should have + if (is_compressing) { + extension = compress_info.value().first.recommended_compressed_extension; + } else { + extension = compress_info.value().first.recommended_uncompressed_extension; + } + + std::string output_path = build_output_path(source_path, extension, output_dir_path.value()); + + bool success = perform_z3ds_operation( + is_compressing, source_path, output_path, compress_info.value().first.underlying_magic, + compress_info.value().second, nullptr, compress_info.value().first.default_metadata); + if (!success) { + FileUtil::Delete(output_path); + std::cout << "fail: Failed to perform Z3DS operation." << common_error_addendum + << std::endl; + return 1; + } + std::cout << "success" << std::endl; + + return 0; +} + +} // namespace CitraCLI diff --git a/src/citra_cli/compression_cli.h b/src/citra_cli/compression_cli.h new file mode 100644 index 000000000..81758f559 --- /dev/null +++ b/src/citra_cli/compression_cli.h @@ -0,0 +1,9 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +namespace CitraCLI { + +int ParseCompressionCommand(int argc, char* argv[]); + +} diff --git a/src/citra_meta/CMakeLists.txt b/src/citra_meta/CMakeLists.txt index a2398b5d6..156799b03 100644 --- a/src/citra_meta/CMakeLists.txt +++ b/src/citra_meta/CMakeLists.txt @@ -68,7 +68,7 @@ if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux" AND MINGW) ) endif() -target_link_libraries(citra_meta PRIVATE citra_common fmt) +target_link_libraries(citra_meta PRIVATE citra_cli citra_common fmt) if (ENABLE_QT) target_link_libraries(citra_meta PRIVATE citra_qt) diff --git a/src/citra_meta/common_strings.h b/src/citra_meta/common_strings.h index 0276b697f..2a71f832e 100644 --- a/src/citra_meta/common_strings.h +++ b/src/citra_meta/common_strings.h @@ -10,6 +10,8 @@ namespace Common { constexpr char help_string[] = "Usage: {} [options] \n" + "-c [path] Z3DS compress a ROM located at the given path\n" + " (optionally provide '-o [path]' for output directory)\n" "-d, --dump-video [path] Dump video recording of emulator playback to the given file path\n" "-f, --fullscreen Start in fullscreen mode\n" "-g, --gdbport [port] Enable gdb stub on the given port\n" @@ -24,6 +26,8 @@ constexpr char help_string[] = "the old citra-room executable)\n" #endif "-v, --version Output version information and exit\n" - "-w, --windowed Start in windowed mode"; + "-w, --windowed Start in windowed mode\n" + "-x [path] Decompress a Z3DS compressed ROM located at the given path\n" + " (optionally provide '-o [path]' for output directory)"; } // namespace Common diff --git a/src/citra_meta/main.cpp b/src/citra_meta/main.cpp index b7a7e5584..c2574c167 100644 --- a/src/citra_meta/main.cpp +++ b/src/citra_meta/main.cpp @@ -4,6 +4,7 @@ #include +#include "citra_cli/citra_cli.h" #include "common/detached_tasks.h" #include "common/scope_exit.h" @@ -59,6 +60,10 @@ int main(int argc, char* argv[]) { } #endif + if (CitraCLI::CheckForOptions(CitraCLI::cli_capture_optstring, argc, argv)) { + return CitraCLI::ParseCommand(argc, argv); + } + #if ENABLE_ROOM bool launch_room = false; for (int i = 1; i < argc; i++) { diff --git a/src/citra_qt/citra_qt.cpp b/src/citra_qt/citra_qt.cpp index bda6d0816..b4335797e 100644 --- a/src/citra_qt/citra_qt.cpp +++ b/src/citra_qt/citra_qt.cpp @@ -3235,64 +3235,6 @@ void GMainWindow::OnDumpVideo() { } } -static std::optional> GetCompressFileInfo( - const std::string& filepath, bool compress) { - Loader::AppLoader::CompressFileInfo compress_info{}; - compress_info.is_supported = false; - size_t frame_size{}; - auto loader = Loader::GetLoader(filepath); - if (loader) { - compress_info = loader->GetCompressFileInfo(); - frame_size = FileUtil::Z3DSWriteIOFile::DEFAULT_FRAME_SIZE; - } else { - bool is_compressed = false; - if (Service::AM::CheckCIAToInstall(filepath, is_compressed, compress ? true : false) == - Service::AM::InstallStatus::Success) { - compress_info.is_supported = true; - compress_info.is_compressed = is_compressed; - compress_info.recommended_compressed_extension = "zcia"; - compress_info.recommended_uncompressed_extension = "cia"; - compress_info.underlying_magic = std::array({'C', 'I', 'A', '\0'}); - frame_size = FileUtil::Z3DSWriteIOFile::DEFAULT_CIA_FRAME_SIZE; - if (compress) { - auto meta_info = Service::AM::GetCIAInfos(filepath); - if (meta_info.Succeeded()) { - const auto& meta_info_val = meta_info.Unwrap(); - std::vector value(sizeof(Service::AM::TitleInfo)); - memcpy(value.data(), &meta_info_val.first, sizeof(Service::AM::TitleInfo)); - compress_info.default_metadata.emplace("titleinfo", value); - if (meta_info_val.second) { - value.resize(sizeof(Loader::SMDH)); - memcpy(value.data(), meta_info_val.second.get(), sizeof(Loader::SMDH)); - compress_info.default_metadata.emplace("smdh", value); - } - } - } - } - } - - if (!compress_info.is_supported) { - LOG_ERROR(Frontend, - "Error {} file {}, the selected file is not a compatible 3DS ROM format or is " - "encrypted.", - compress ? "compressing" : "decompressing", filepath); - return {}; - } - if (compress_info.is_compressed && compress) { - LOG_ERROR(Frontend, "Error compressing file {}, the selected file is already compressed", - filepath); - return {}; - } - if (!compress_info.is_compressed && !compress) { - LOG_ERROR(Frontend, - "Error decompressing file {}, the selected file is already decompressed", - filepath); - return {}; - } - - return std::pair(compress_info, frame_size); -} - void GMainWindow::OnCompressFile() { // NOTE: Encrypted files SHOULD NEVER be compressed, otherwise the resulting // compressed file will have very poor compression ratios, due to the high @@ -3315,7 +3257,7 @@ void GMainWindow::OnCompressFile() { bool single_file = filepaths.size() == 1; if (single_file) { // If it's a single file, ask the user for the output file. - auto compress_info = GetCompressFileInfo(filepaths[0].toStdString(), true); + auto compress_info = Loader::GetCompressFileInfo(filepaths[0].toStdString(), true); if (!compress_info.has_value()) { emit CompressFinished(true, false); return; @@ -3355,7 +3297,7 @@ void GMainWindow::OnCompressFile() { std::string in_path = filepath.toStdString(); // Identify file type - auto compress_info = GetCompressFileInfo(filepath.toStdString(), true); + auto compress_info = Loader::GetCompressFileInfo(filepath.toStdString(), true); if (!compress_info.has_value()) { total_success = false; continue; @@ -3408,7 +3350,7 @@ void GMainWindow::OnDecompressFile() { bool single_file = filepaths.size() == 1; if (single_file) { // If it's a single file, ask the user for the output file. - auto compress_info = GetCompressFileInfo(filepaths[0].toStdString(), false); + auto compress_info = Loader::GetCompressFileInfo(filepaths[0].toStdString(), false); if (!compress_info.has_value()) { emit CompressFinished(false, false); return; @@ -3449,7 +3391,7 @@ void GMainWindow::OnDecompressFile() { std::string in_path = filepath.toStdString(); // Identify file type - auto compress_info = GetCompressFileInfo(filepath.toStdString(), false); + auto compress_info = Loader::GetCompressFileInfo(filepath.toStdString(), false); if (!compress_info.has_value()) { total_success = false; continue; diff --git a/src/core/loader/loader.cpp b/src/core/loader/loader.cpp index f1d610d9e..ccd56ec0b 100644 --- a/src/core/loader/loader.cpp +++ b/src/core/loader/loader.cpp @@ -6,8 +6,10 @@ #include #include "common/logging/log.h" #include "common/string_util.h" +#include "common/zstd_compression.h" #include "core/core.h" #include "core/hle/kernel/process.h" +#include "core/hle/service/am/am.h" #include "core/loader/3dsx.h" #include "core/loader/artic.h" #include "core/loader/elf.h" @@ -179,4 +181,62 @@ std::unique_ptr GetLoader(const std::string& filename) { return GetFileLoader(system, std::move(file), type, filename_filename, filename); } +std::optional> GetCompressFileInfo( + const std::string& filepath, bool compress) { + Loader::AppLoader::CompressFileInfo compress_info{}; + compress_info.is_supported = false; + size_t frame_size{}; + auto loader = Loader::GetLoader(filepath); + if (loader) { + compress_info = loader->GetCompressFileInfo(); + frame_size = FileUtil::Z3DSWriteIOFile::DEFAULT_FRAME_SIZE; + } else { + bool is_compressed = false; + if (Service::AM::CheckCIAToInstall(filepath, is_compressed, compress ? true : false) == + Service::AM::InstallStatus::Success) { + compress_info.is_supported = true; + compress_info.is_compressed = is_compressed; + compress_info.recommended_compressed_extension = "zcia"; + compress_info.recommended_uncompressed_extension = "cia"; + compress_info.underlying_magic = std::array({'C', 'I', 'A', '\0'}); + frame_size = FileUtil::Z3DSWriteIOFile::DEFAULT_CIA_FRAME_SIZE; + if (compress) { + auto meta_info = Service::AM::GetCIAInfos(filepath); + if (meta_info.Succeeded()) { + const auto& meta_info_val = meta_info.Unwrap(); + std::vector value(sizeof(Service::AM::TitleInfo)); + memcpy(value.data(), &meta_info_val.first, sizeof(Service::AM::TitleInfo)); + compress_info.default_metadata.emplace("titleinfo", value); + if (meta_info_val.second) { + value.resize(sizeof(Loader::SMDH)); + memcpy(value.data(), meta_info_val.second.get(), sizeof(Loader::SMDH)); + compress_info.default_metadata.emplace("smdh", value); + } + } + } + } + } + + if (!compress_info.is_supported) { + LOG_ERROR(Frontend, + "Error {} file {}, the selected file is not a compatible 3DS ROM format or is " + "encrypted.", + compress ? "compressing" : "decompressing", filepath); + return {}; + } + if (compress_info.is_compressed && compress) { + LOG_ERROR(Frontend, "Error compressing file {}, the selected file is already compressed", + filepath); + return {}; + } + if (!compress_info.is_compressed && !compress) { + LOG_ERROR(Frontend, + "Error decompressing file {}, the selected file is already decompressed", + filepath); + return {}; + } + + return std::pair(compress_info, frame_size); +} + } // namespace Loader diff --git a/src/core/loader/loader.h b/src/core/loader/loader.h index 678b48bf7..97e8e386e 100644 --- a/src/core/loader/loader.h +++ b/src/core/loader/loader.h @@ -322,4 +322,7 @@ protected: */ std::unique_ptr GetLoader(const std::string& filename); +std::optional> GetCompressFileInfo( + const std::string& filepath, bool compress); + } // namespace Loader