qt: Add batch Z3DS compression/decompression

This commit is contained in:
PabloMK7 2025-12-07 00:06:27 +01:00 committed by OpenSauce
parent 6b05944116
commit 4fdc45a771
2 changed files with 236 additions and 139 deletions

View File

@ -2290,8 +2290,8 @@ void GMainWindow::OnMenuBootHomeMenu(u32 region) {
void GMainWindow::InstallCIA(QStringList filepaths) {
ui->action_Install_CIA->setEnabled(false);
game_list->SetDirectoryWatcherEnabled(false);
progress_bar->show();
progress_bar->setMaximum(INT_MAX);
emit UpdateProgress(0, 0);
(void)QtConcurrent::run([&, filepaths] {
Service::AM::InstallStatus status;
@ -2307,6 +2307,11 @@ void GMainWindow::InstallCIA(QStringList filepaths) {
}
void GMainWindow::OnUpdateProgress(std::size_t written, std::size_t total) {
if (written == 0 and total == 0) {
progress_bar->show();
progress_bar->setValue(0);
progress_bar->setMaximum(INT_MAX);
}
progress_bar->setValue(
static_cast<int>(INT_MAX * (static_cast<double>(written) / static_cast<double>(total))));
}
@ -2350,11 +2355,19 @@ void GMainWindow::OnCompressFinished(bool is_compress, bool success) {
if (!success) {
if (is_compress) {
QMessageBox::critical(this, tr("Error compressing file"),
tr("File compress operation failed, check log for details."));
QMessageBox::critical(this, tr("Z3DS Compression"),
tr("Failed to compress some files, check log for details."));
} else {
QMessageBox::critical(this, tr("Error decompressing file"),
tr("File decompress operation failed, check log for details."));
QMessageBox::critical(this, tr("Z3DS Compression"),
tr("Failed to decompress some files, check log for details."));
}
} else {
if (is_compress) {
QMessageBox::information(this, tr("Z3DS Compression"),
tr("All files have been compressed successfully."));
} else {
QMessageBox::information(this, tr("Z3DS Compression"),
tr("All files have been decompressed successfully."));
}
}
}
@ -3066,43 +3079,27 @@ void GMainWindow::OnDumpVideo() {
}
}
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
// entropy caused by encryption. This may cause confusion to the user as they
// will see the files do not compress well and blame the emulator.
//
// This is enforced using the loaders as they already return an error on encryption.
QString filepath = QFileDialog::getOpenFileName(
this, tr("Load 3DS ROM File"), UISettings::values.roms_path,
tr("3DS ROM Files (*.cia *cci *3dsx *cxi)") + QStringLiteral(";;") + tr("All Files (*.*)"));
if (filepath.isEmpty()) {
return;
}
std::string in_path = filepath.toStdString();
// Identify file type
static std::optional<std::pair<Loader::AppLoader::CompressFileInfo, size_t>> 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(in_path);
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(in_path, is_compressed, true) ==
if (Service::AM::CheckCIAToInstall(filepath, is_compressed, compress ? true : false) ==
Service::AM::InstallStatus::Success) {
auto meta_info = Service::AM::GetCIAInfos(in_path);
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<u8, 4>({'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<u8> value(sizeof(Service::AM::TitleInfo));
@ -3117,122 +3114,218 @@ void GMainWindow::OnCompressFile() {
}
}
}
if (!compress_info.is_supported) {
QMessageBox::critical(
this, tr("Error compressing file"),
tr("The selected file is not a compatible 3DS ROM format. Make sure you have "
"chosen the right file, and that it is not encrypted."));
return;
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) {
QMessageBox::warning(this, tr("Error compressing file"),
tr("The selected file is already compressed."));
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
// entropy caused by encryption. This may cause confusion to the user as they
// will see the files do not compress well and blame the emulator.
//
// This is enforced using the loaders as they already return an error on encryption.
QStringList filepaths =
QFileDialog::getOpenFileNames(this, tr("Load 3DS ROM Files"), UISettings::values.roms_path,
tr("3DS ROM Files (*.cia *.cci *.3dsx *.cxi)") +
QStringLiteral(";;") + tr("All Files (*.*)"));
QString out_path;
if (filepaths.isEmpty()) {
return;
}
QString out_filter =
tr("3DS Compressed ROM File (*.%1)")
.arg(QString::fromStdString(compress_info.recommended_compressed_extension));
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);
if (!compress_info.has_value()) {
emit CompressFinished(true, false);
return;
}
QFileInfo fileinfo(filepath);
QString final_path = fileinfo.path() + QStringLiteral(DIR_SEP) + fileinfo.completeBaseName() +
QFileInfo fileinfo(filepaths[0]);
QString final_path =
fileinfo.path() + QStringLiteral(DIR_SEP) + fileinfo.completeBaseName() +
QStringLiteral(".") +
QString::fromStdString(compress_info.recommended_compressed_extension);
QString::fromStdString(compress_info.value().first.recommended_compressed_extension);
filepath = QFileDialog::getSaveFileName(this, tr("Save 3DS Compressed ROM File"), final_path,
out_filter);
if (filepath.isEmpty()) {
QString out_filter = tr("3DS Compressed ROM File (*.%1)")
.arg(QString::fromStdString(
compress_info.value().first.recommended_compressed_extension));
out_path = QFileDialog::getSaveFileName(this, tr("Save 3DS Compressed ROM File"),
final_path, out_filter);
if (out_path.isEmpty()) {
return;
}
std::string out_path = filepath.toStdString();
progress_bar->show();
progress_bar->setMaximum(INT_MAX);
(void)QtConcurrent::run([&, in_path, out_path, compress_info, frame_size] {
const auto progress = [&](std::size_t written, std::size_t total) {
emit UpdateProgress(written, total);
};
bool success =
FileUtil::CompressZ3DSFile(in_path, out_path, compress_info.underlying_magic,
frame_size, progress, compress_info.default_metadata);
if (!success) {
FileUtil::Delete(out_path);
}
emit OnCompressFinished(true, success);
});
}
void GMainWindow::OnDecompressFile() {
QString filepath = QFileDialog::getOpenFileName(
this, tr("Load 3DS Compressed ROM File"), UISettings::values.roms_path,
tr("3DS Compressed ROM Files (*.zcia *zcci *z3dsx *zcxi)") + QStringLiteral(";;") +
tr("All Files (*.*)"));
if (filepath.isEmpty()) {
} else {
// Otherwise, ask the user the directory to output the files.
out_path = QFileDialog::getExistingDirectory(
this, tr("Select Output 3DS Compressed ROM Folder"), UISettings::values.roms_path,
QFileDialog::ShowDirsOnly);
if (out_path.isEmpty()) {
return;
}
}
(void)QtConcurrent::run([&, filepaths, out_path] {
bool single_file = filepaths.size() == 1;
QString out_filepath;
bool total_success = true;
for (const QString& filepath : filepaths) {
std::string in_path = filepath.toStdString();
// Identify file type
Loader::AppLoader::CompressFileInfo compress_info{};
compress_info.is_supported = false;
{
auto loader = Loader::GetLoader(in_path);
if (loader) {
compress_info = loader->GetCompressFileInfo();
auto compress_info = GetCompressFileInfo(filepath.toStdString(), true);
if (!compress_info.has_value()) {
total_success = false;
continue;
}
if (single_file) {
out_filepath = out_path;
} else {
bool is_compressed = false;
if (Service::AM::CheckCIAToInstall(in_path, is_compressed, 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<u8, 4>({'C', 'I', 'A', '\0'});
}
}
}
if (!compress_info.is_supported) {
QMessageBox::critical(this, tr("Error decompressing file"),
tr("The selected file is not a compatible compressed 3DS ROM format. "
"Make sure you have "
"chosen the right file."));
return;
}
if (!compress_info.is_compressed) {
QMessageBox::warning(this, tr("Error decompressing file"),
tr("The selected file is already decompressed."));
return;
}
QString out_filter =
tr("3DS ROM File (*.%1)")
.arg(QString::fromStdString(compress_info.recommended_uncompressed_extension));
QFileInfo fileinfo(filepath);
QString final_path = fileinfo.path() + QStringLiteral(DIR_SEP) + fileinfo.completeBaseName() +
out_filepath = out_path + QStringLiteral(DIR_SEP) + fileinfo.completeBaseName() +
QStringLiteral(".") +
QString::fromStdString(compress_info.recommended_uncompressed_extension);
filepath = QFileDialog::getSaveFileName(this, tr("Save 3DS ROM File"), final_path, out_filter);
if (filepath.isEmpty()) {
return;
QString::fromStdString(
compress_info.value().first.recommended_compressed_extension);
}
std::string out_path = filepath.toStdString();
progress_bar->show();
progress_bar->setMaximum(INT_MAX);
std::string out_path = out_filepath.toStdString();
emit UpdateProgress(0, 0);
(void)QtConcurrent::run([&, in_path, out_path, compress_info] {
const auto progress = [&](std::size_t written, std::size_t total) {
emit UpdateProgress(written, total);
};
bool success = FileUtil::CompressZ3DSFile(in_path, out_path,
compress_info.value().first.underlying_magic,
compress_info.value().second, progress,
compress_info.value().first.default_metadata);
if (!success) {
total_success = false;
FileUtil::Delete(out_path);
}
}
emit CompressFinished(true, total_success);
});
}
void GMainWindow::OnDecompressFile() {
QStringList filepaths = QFileDialog::getOpenFileNames(
this, tr("Load 3DS Compressed ROM Files"), UISettings::values.roms_path,
tr("3DS Compressed ROM Files (*.zcia *zcci *z3dsx *zcxi)") + QStringLiteral(";;") +
tr("All Files (*.*)"));
QString out_path;
if (filepaths.isEmpty()) {
return;
}
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);
if (!compress_info.has_value()) {
emit CompressFinished(false, false);
return;
}
QFileInfo fileinfo(filepaths[0]);
QString final_path =
fileinfo.path() + QStringLiteral(DIR_SEP) + fileinfo.completeBaseName() +
QStringLiteral(".") +
QString::fromStdString(compress_info.value().first.recommended_uncompressed_extension);
QString out_filter =
tr("3DS ROM File (*.%1)")
.arg(QString::fromStdString(
compress_info.value().first.recommended_uncompressed_extension));
out_path =
QFileDialog::getSaveFileName(this, tr("Save 3DS ROM File"), final_path, out_filter);
if (out_path.isEmpty()) {
return;
}
} else {
// Otherwise, ask the user the directory to output the files.
out_path = QFileDialog::getExistingDirectory(this, tr("Select Output 3DS ROM Folder"),
UISettings::values.roms_path,
QFileDialog::ShowDirsOnly);
if (out_path.isEmpty()) {
return;
}
}
(void)QtConcurrent::run([&, filepaths, out_path] {
bool single_file = filepaths.size() == 1;
QString out_filepath;
bool total_success = true;
for (const QString& filepath : filepaths) {
std::string in_path = filepath.toStdString();
// Identify file type
auto compress_info = GetCompressFileInfo(filepath.toStdString(), false);
if (!compress_info.has_value()) {
total_success = false;
continue;
}
if (single_file) {
out_filepath = out_path;
} else {
QFileInfo fileinfo(filepath);
out_filepath = out_path + QStringLiteral(DIR_SEP) + fileinfo.completeBaseName() +
QStringLiteral(".") +
QString::fromStdString(
compress_info.value().first.recommended_uncompressed_extension);
}
std::string out_path = out_filepath.toStdString();
emit UpdateProgress(0, 0);
const auto progress = [&](std::size_t written, std::size_t total) {
emit UpdateProgress(written, total);
};
// TODO(PabloMK7): What should we do with the metadata?
bool success = FileUtil::DeCompressZ3DSFile(in_path, out_path, progress);
if (!success) {
total_success = false;
FileUtil::Delete(out_path);
}
emit OnCompressFinished(false, success);
}
emit CompressFinished(false, total_success);
});
}

View File

@ -164,7 +164,11 @@ std::unique_ptr<AppLoader> GetLoader(const std::string& filename) {
FileType filename_type = GuessFromExtension(filename_extension);
if (type != filename_type) {
// Do not show the error for CIA files, as their type cannot be determined.
if (!(type == FileType::Unknown && filename_type == FileType::CIA)) {
LOG_WARNING(Loader, "File {} has a different type than its extension.", filename);
}
if (FileType::Unknown == type)
type = filename_type;
}