From 8ae3ff887ac26335edde20f487481d4d47246fb9 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Sun, 2 Nov 2025 19:25:13 +0900 Subject: [PATCH] [UI] Fix bad UTF8 related crashes in Qt string processing --- src/xenia/app/emulator_window.cc | 10 ++-- src/xenia/ui/achievements_dialog_qt.cc | 11 ++-- src/xenia/ui/config_dialog_qt.cc | 55 ++++++++++--------- src/xenia/ui/content_install_dialog_qt.cc | 12 +++-- src/xenia/ui/file_picker_qt.cc | 5 +- src/xenia/ui/game_config_dialog_qt.cc | 66 ++++++++++++----------- src/xenia/ui/game_list_dialog_qt.cc | 56 +++++++++++-------- src/xenia/ui/postprocessing_dialog_qt.cc | 11 ++-- src/xenia/ui/profile_dialog_qt.cc | 15 +++--- src/xenia/ui/profile_editor_dialog_qt.cc | 31 +++++------ src/xenia/ui/qt_util.h | 35 ++++++++++++ src/xenia/ui/xmp_dialog_qt.cc | 10 ++-- 12 files changed, 190 insertions(+), 127 deletions(-) create mode 100644 src/xenia/ui/qt_util.h diff --git a/src/xenia/app/emulator_window.cc b/src/xenia/app/emulator_window.cc index 7162c4f4b..b2f7aa48c 100644 --- a/src/xenia/app/emulator_window.cc +++ b/src/xenia/app/emulator_window.cc @@ -39,6 +39,7 @@ #include "xenia/ui/notification_widget_qt.h" #include "xenia/ui/postprocessing_dialog_qt.h" #include "xenia/ui/profile_dialog_qt.h" +#include "xenia/ui/qt_util.h" #include "xenia/ui/xmp_dialog_qt.h" #if XE_PLATFORM_WIN32 @@ -219,6 +220,7 @@ namespace app { using xe::ui::FileDropEvent; using xe::ui::KeyEvent; using xe::ui::MenuItem; +using xe::ui::SafeQString; using xe::ui::UIEvent; using namespace xe::hid; @@ -1113,9 +1115,9 @@ void EmulatorWindow::ExportScreenshot(const xe::ui::RawImage& image) { app_context_.CallInUIThread([this, notification_text]() { auto* qt_window = dynamic_cast(window_.get()); if (qt_window) { - auto* notification = new NotificationWidgetQt( - qt_window->qwindow(), "Screenshot Created!", - QString::fromStdString(notification_text), 3000); + auto* notification = + new NotificationWidgetQt(qt_window->qwindow(), "Screenshot Created!", + SafeQString(notification_text), 3000); notification->Show(); } }); @@ -2151,7 +2153,7 @@ void EmulatorWindow::DisplayHotKeysConfig() { // Show Qt message box QMessageBox msgBox; msgBox.setWindowTitle("Controller Hotkeys"); - msgBox.setText(QString::fromStdString(msg)); + msgBox.setText(SafeQString(msg)); msgBox.setIcon(QMessageBox::Information); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.exec(); diff --git a/src/xenia/ui/achievements_dialog_qt.cc b/src/xenia/ui/achievements_dialog_qt.cc index 1e20e99da..925d2654e 100644 --- a/src/xenia/ui/achievements_dialog_qt.cc +++ b/src/xenia/ui/achievements_dialog_qt.cc @@ -33,6 +33,7 @@ #include "xenia/kernel/xam/achievement_manager.h" #include "xenia/kernel/xam/profile_manager.h" #include "xenia/kernel/xam/xam_state.h" +#include "xenia/ui/qt_util.h" namespace xe { namespace kernel { @@ -45,6 +46,8 @@ struct Achievement; namespace xe { namespace ui { +using xe::ui::SafeQString; + AchievementsDialogQt::AchievementsDialogQt( QWidget* parent, kernel::KernelState* kernel_state, const kernel::xam::TitleInfo* title_info, @@ -401,7 +404,7 @@ QString AchievementsDialogQt::GetAchievementTitle( if (!title.empty() && title.back() == '\0') { title.pop_back(); } - return QString::fromStdString(title); + return SafeQString(title); } QString AchievementsDialogQt::GetAchievementDescription( @@ -415,7 +418,7 @@ QString AchievementsDialogQt::GetAchievementDescription( if (!desc.empty() && desc.back() == '\0') { desc.pop_back(); } - return QString::fromStdString(desc); + return SafeQString(desc); } else { // Hide description when checkbox is not checked return QString(); @@ -428,7 +431,7 @@ QString AchievementsDialogQt::GetAchievementDescription( if (!desc.empty() && desc.back() == '\0') { desc.pop_back(); } - return QString::fromStdString(desc); + return SafeQString(desc); } QString AchievementsDialogQt::GetUnlockedTime( @@ -442,7 +445,7 @@ QString AchievementsDialogQt::GetUnlockedTime( chrono::WinSystemClock::to_sys(achievement.unlock_time.to_time_point()); auto unlock_time = std::chrono::system_clock::to_time_t(unlock_tp); - return QString::fromStdString( + return SafeQString( fmt::format("{:%Y-%m-%d %H:%M}", fmt::localtime(unlock_time))); } diff --git a/src/xenia/ui/config_dialog_qt.cc b/src/xenia/ui/config_dialog_qt.cc index 903185ae3..6b515ee82 100644 --- a/src/xenia/ui/config_dialog_qt.cc +++ b/src/xenia/ui/config_dialog_qt.cc @@ -28,6 +28,7 @@ #include "xenia/base/logging.h" #include "xenia/config.h" #include "xenia/ui/config_helpers.h" +#include "xenia/ui/qt_util.h" #if XE_PLATFORM_LINUX #include @@ -40,6 +41,8 @@ namespace { // Use the shared enum options from config_helpers.h using xe::ui::GetKnownEnumOptions; +using xe::ui::SafeQString; +using xe::ui::SafeStdString; #if XE_PLATFORM_LINUX // Check if a command exists in PATH by searching each directory @@ -178,7 +181,7 @@ void ConfigDialogQt::SetupUI() { // Add categories in order for (const auto& category_name : category_order_) { - category_list_->addItem(QString::fromStdString(category_name)); + category_list_->addItem(SafeQString(category_name)); auto it = categories_.find(category_name); if (it != categories_.end()) { CreateCategoryPage(category_name, it->second); @@ -227,14 +230,13 @@ void ConfigDialogQt::CreateCategoryPage( for (auto* var_info : vars) { // Create label with tooltip - left aligned - auto* label = new QLabel(QString::fromStdString(var_info->name)); + auto* label = new QLabel(SafeQString(var_info->name)); label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); if (!var_info->description.empty()) { // Format tooltip with rich text to enable word wrapping QString tooltip = QString("

%1

") - .arg(QString::fromStdString(var_info->description) - .toHtmlEscaped()); + .arg(SafeQString(var_info->description).toHtmlEscaped()); label->setToolTip(tooltip); label->setToolTipDuration(5000); // Show for 5 seconds } @@ -297,7 +299,7 @@ QWidget* ConfigDialogQt::CreateEditorWidget(ConfigVarInfo* var_info) { int current_index = -1; for (size_t i = 0; i < options.size(); ++i) { - combo->addItem(QString::fromStdString(options[i])); + combo->addItem(SafeQString(options[i])); if (options[i] == var_info->pending_value) { current_index = static_cast(i); } @@ -318,22 +320,21 @@ QWidget* ConfigDialogQt::CreateEditorWidget(ConfigVarInfo* var_info) { layout->setContentsMargins(0, 0, 0, 0); auto* line_edit = new QLineEdit(); - line_edit->setText(QString::fromStdString(var_info->pending_value)); + line_edit->setText(SafeQString(var_info->pending_value)); connect(line_edit, &QLineEdit::textChanged, this, &ConfigDialogQt::OnValueChanged); layout->addWidget(line_edit); auto* browse_button = new QPushButton("Browse..."); - connect( - browse_button, &QPushButton::clicked, [this, line_edit, var_info]() { - QString path = QFileDialog::getExistingDirectory( - this, - QString::fromStdString("Select Directory for " + var_info->name), - line_edit->text()); - if (!path.isEmpty()) { - line_edit->setText(path); - } - }); + connect(browse_button, &QPushButton::clicked, + [this, line_edit, var_info]() { + QString path = QFileDialog::getExistingDirectory( + this, SafeQString("Select Directory for " + var_info->name), + line_edit->text()); + if (!path.isEmpty()) { + line_edit->setText(path); + } + }); layout->addWidget(browse_button); return container; @@ -358,7 +359,7 @@ QWidget* ConfigDialogQt::CreateEditorWidget(ConfigVarInfo* var_info) { } else { // Text input for all other types (string, double, etc.) auto* line_edit = new QLineEdit(); - line_edit->setText(QString::fromStdString(var_info->pending_value)); + line_edit->setText(SafeQString(var_info->pending_value)); connect(line_edit, &QLineEdit::textChanged, this, &ConfigDialogQt::OnValueChanged); return line_edit; @@ -370,16 +371,16 @@ std::string ConfigDialogQt::GetEditorValue(QWidget* editor, if (auto* checkbox = qobject_cast(editor)) { return checkbox->isChecked() ? "true" : "false"; } else if (auto* combo = qobject_cast(editor)) { - return combo->currentText().toStdString(); + return SafeStdString(combo->currentText()); } else if (auto* spinbox = qobject_cast(editor)) { return std::to_string(spinbox->value()); } else if (auto* line_edit = qobject_cast(editor)) { - return line_edit->text().toStdString(); + return SafeStdString(line_edit->text()); } else if (auto* container = qobject_cast(editor)) { // For path containers, find the QLineEdit child auto* line_edit = container->findChild(); if (line_edit) { - return line_edit->text().toStdString(); + return SafeStdString(line_edit->text()); } } return ""; @@ -548,7 +549,7 @@ void ConfigDialogQt::ResetToDefaults() { checkbox->setChecked(default_value == "true"); } else if (auto* combo = qobject_cast(var_info.editor_widget)) { - int index = combo->findText(QString::fromStdString(default_value)); + int index = combo->findText(SafeQString(default_value)); if (index >= 0) { combo->setCurrentIndex(index); } @@ -561,12 +562,12 @@ void ConfigDialogQt::ResetToDefaults() { } } else if (auto* line_edit = qobject_cast(var_info.editor_widget)) { - line_edit->setText(QString::fromStdString(default_value)); + line_edit->setText(SafeQString(default_value)); } else if (auto* container = qobject_cast(var_info.editor_widget)) { auto* line_edit = container->findChild(); if (line_edit) { - line_edit->setText(QString::fromStdString(default_value)); + line_edit->setText(SafeQString(default_value)); } } } @@ -655,8 +656,7 @@ void ConfigDialogQt::OnDiscardClicked() { checkbox->setChecked(var_info.current_value == "true"); } else if (auto* combo = qobject_cast(var_info.editor_widget)) { - int index = - combo->findText(QString::fromStdString(var_info.current_value)); + int index = combo->findText(SafeQString(var_info.current_value)); if (index >= 0) { combo->setCurrentIndex(index); } @@ -669,13 +669,12 @@ void ConfigDialogQt::OnDiscardClicked() { } } else if (auto* line_edit = qobject_cast(var_info.editor_widget)) { - line_edit->setText(QString::fromStdString(var_info.current_value)); + line_edit->setText(SafeQString(var_info.current_value)); } else if (auto* container = qobject_cast(var_info.editor_widget)) { auto* line_edit = container->findChild(); if (line_edit) { - line_edit->setText( - QString::fromStdString(var_info.current_value)); + line_edit->setText(SafeQString(var_info.current_value)); } } } diff --git a/src/xenia/ui/content_install_dialog_qt.cc b/src/xenia/ui/content_install_dialog_qt.cc index 17cfc0795..25903bff2 100644 --- a/src/xenia/ui/content_install_dialog_qt.cc +++ b/src/xenia/ui/content_install_dialog_qt.cc @@ -21,11 +21,14 @@ #include "third_party/fmt/include/fmt/format.h" #include "xenia/base/system.h" +#include "xenia/ui/qt_util.h" #include "xenia/xbox.h" namespace xe { namespace ui { +using xe::ui::SafeQString; + ContentInstallDialogQt::ContentInstallDialogQt( QWidget* parent, const std::filesystem::path& content_root, std::shared_ptr> entries) @@ -199,11 +202,11 @@ void ContentInstallDialogQt::UpdateProgress() { // Update name widgets.name_label->setText( - QString("Name: %1").arg(QString::fromStdString(entry.name_))); + QString("Name: %1").arg(SafeQString(entry.name_))); // Update path with link QString path_str = - QString::fromStdString(xe::path_to_utf8(entry.data_installation_path_)); + SafeQString(xe::path_to_utf8(entry.data_installation_path_)); widgets.path_label->setText( QString("Installation Path: %1").arg(path_str)); @@ -212,8 +215,7 @@ void ContentInstallDialogQt::UpdateProgress() { auto it = XContentTypeMap.find(entry.content_type_); if (it != XContentTypeMap.end()) { widgets.type_label->setText( - QString("Content Type: %1") - .arg(QString::fromStdString(it->second))); + QString("Content Type: %1").arg(SafeQString(it->second))); } } else { widgets.type_label->setText(""); @@ -230,7 +232,7 @@ void ContentInstallDialogQt::UpdateProgress() { entry.installation_result_); } - widgets.status_label->setText(QString::fromStdString(result)); + widgets.status_label->setText(SafeQString(result)); // Update progress bar if (entry.content_size_ > 0) { diff --git a/src/xenia/ui/file_picker_qt.cc b/src/xenia/ui/file_picker_qt.cc index c16372b5f..2709a8f4b 100644 --- a/src/xenia/ui/file_picker_qt.cc +++ b/src/xenia/ui/file_picker_qt.cc @@ -18,6 +18,7 @@ #include "xenia/base/filesystem.h" #include "xenia/base/logging.h" #include "xenia/base/string.h" +#include "xenia/ui/qt_util.h" #include "xenia/ui/window_qt.h" #if XE_PLATFORM_WIN32 @@ -27,6 +28,8 @@ namespace xe { namespace ui { +using xe::ui::SafeQString; + #if XE_PLATFORM_WIN32 // Detect if running under Wine by checking for wine_get_version in ntdll static bool IsRunningOnWine() { @@ -85,7 +88,7 @@ bool QtFilePicker::Show(Window* parent_window) { } // Use the custom title set via set_title(), or default based on mode - QString title = QString::fromStdString(this->title()); + QString title = SafeQString(this->title()); auto* qt_window = parent_window ? dynamic_cast(parent_window) : nullptr; diff --git a/src/xenia/ui/game_config_dialog_qt.cc b/src/xenia/ui/game_config_dialog_qt.cc index d3b3c6513..1e8b8450a 100644 --- a/src/xenia/ui/game_config_dialog_qt.cc +++ b/src/xenia/ui/game_config_dialog_qt.cc @@ -36,6 +36,7 @@ #include "xenia/base/logging.h" #include "xenia/config.h" #include "xenia/ui/config_helpers.h" +#include "xenia/ui/qt_util.h" namespace xe { namespace app { @@ -44,6 +45,8 @@ namespace { // Use the shared enum options from config_helpers.h using xe::ui::GetKnownEnumOptions; +using xe::ui::SafeQString; +using xe::ui::SafeStdString; // Helper to convert a RapidJSON value to string std::string JsonValueToString(const rapidjson::Value& value) { @@ -114,8 +117,8 @@ GameConfigDialogQt::GameConfigDialogQt(QWidget* parent, GameConfigDialogQt::~GameConfigDialogQt() = default; void GameConfigDialogQt::SetupUI() { - setWindowTitle(QString::fromStdString( - fmt::format("Game Config Overrides - {}", game_title_))); + setWindowTitle( + SafeQString(fmt::format("Game Config Overrides - {}", game_title_))); setMinimumSize(800, 500); resize(900, 600); @@ -253,8 +256,7 @@ void GameConfigDialogQt::LoadConfigOverrides() { overrides_table_->insertRow(row); // Column 0: Variable name (non-editable text) - auto* name_item = - new QTableWidgetItem(QString::fromStdString(var_name)); + auto* name_item = new QTableWidgetItem(SafeQString(var_name)); name_item->setFlags(name_item->flags() & ~Qt::ItemIsEditable); overrides_table_->setItem(row, 0, name_item); @@ -276,9 +278,9 @@ void GameConfigDialogQt::LoadConfigOverrides() { } catch (const std::exception& e) { XELOGE("Failed to load game config {}: {}", xe::path_to_utf8(config_path), e.what()); - QMessageBox::warning(this, "Error Loading Config", - QString::fromStdString(fmt::format( - "Failed to load game config: {}", e.what()))); + QMessageBox::warning( + this, "Error Loading Config", + SafeQString(fmt::format("Failed to load game config: {}", e.what()))); } has_unsaved_changes_ = false; @@ -303,7 +305,7 @@ void GameConfigDialogQt::SaveConfigOverrides() { continue; } - std::string var_name = var_item->text().toStdString(); + std::string var_name = SafeStdString(var_item->text()); // Get value from the cell widget, not from a text item QWidget* value_widget = overrides_table_->cellWidget(row, 1); @@ -391,9 +393,9 @@ void GameConfigDialogQt::SaveConfigOverrides() { } catch (const std::exception& e) { XELOGE("Failed to save game config {}: {}", xe::path_to_utf8(config_path), e.what()); - QMessageBox::critical(this, "Error Saving Config", - QString::fromStdString(fmt::format( - "Failed to save game config: {}", e.what()))); + QMessageBox::critical( + this, "Error Saving Config", + SafeQString(fmt::format("Failed to save game config: {}", e.what()))); } } @@ -412,7 +414,7 @@ QWidget* GameConfigDialogQt::CreateEditorForCvar( auto* combo = new QComboBox(); combo->addItem("true"); combo->addItem("false"); - combo->setCurrentText(QString::fromStdString(current_value)); + combo->setCurrentText(SafeQString(current_value)); connect(combo, QOverload::of(&QComboBox::currentIndexChanged), [this, combo]() { has_unsaved_changes_ = true; @@ -432,7 +434,7 @@ QWidget* GameConfigDialogQt::CreateEditorForCvar( int current_index = -1; for (size_t i = 0; i < options.size(); ++i) { - combo->addItem(QString::fromStdString(options[i])); + combo->addItem(SafeQString(options[i])); if (options[i] == current_value) { current_index = static_cast(i); } @@ -461,7 +463,7 @@ QWidget* GameConfigDialogQt::CreateEditorForCvar( layout->setContentsMargins(0, 0, 0, 0); auto* line_edit = new QLineEdit(); - line_edit->setText(QString::fromStdString(current_value)); + line_edit->setText(SafeQString(current_value)); connect(line_edit, &QLineEdit::textChanged, [this, container]() { has_unsaved_changes_ = true; // Find which row this widget belongs to and update its bold state @@ -477,7 +479,7 @@ QWidget* GameConfigDialogQt::CreateEditorForCvar( auto* browse_button = new QPushButton("Browse..."); connect(browse_button, &QPushButton::clicked, [this, line_edit, var]() { QString path = QFileDialog::getExistingDirectory( - this, QString::fromStdString("Select Directory for " + var->name()), + this, SafeQString("Select Directory for " + var->name()), line_edit->text()); if (!path.isEmpty()) { line_edit->setText(path); @@ -515,7 +517,7 @@ QWidget* GameConfigDialogQt::CreateEditorForCvar( } else { // Text input for all other types (string, double, etc.) auto* line_edit = new QLineEdit(); - line_edit->setText(QString::fromStdString(current_value)); + line_edit->setText(SafeQString(current_value)); connect(line_edit, &QLineEdit::textChanged, [this, line_edit]() { has_unsaved_changes_ = true; // Find which row this widget belongs to and update its bold state @@ -532,16 +534,16 @@ QWidget* GameConfigDialogQt::CreateEditorForCvar( std::string GameConfigDialogQt::GetEditorValue(QWidget* editor) { if (auto* combo = qobject_cast(editor)) { - return combo->currentText().toStdString(); + return SafeStdString(combo->currentText()); } else if (auto* spinbox = qobject_cast(editor)) { return std::to_string(spinbox->value()); } else if (auto* line_edit = qobject_cast(editor)) { - return line_edit->text().toStdString(); + return SafeStdString(line_edit->text()); } else if (auto* container = qobject_cast(editor)) { // For path containers, find the QLineEdit child auto* line_edit = container->findChild(); if (line_edit) { - return line_edit->text().toStdString(); + return SafeStdString(line_edit->text()); } } return ""; @@ -553,7 +555,7 @@ void GameConfigDialogQt::UpdateRowModifiedState(int row) { return; } - std::string var_name = name_item->text().toStdString(); + std::string var_name = SafeStdString(name_item->text()); QWidget* value_widget = overrides_table_->cellWidget(row, 1); if (!value_widget) { return; @@ -599,8 +601,8 @@ void GameConfigDialogQt::OnAddOverrideClicked() { for (const auto& name : cvar_names) { auto* var = (*cvar::ConfigVars)[name]; QString item_text = - QString::fromStdString(fmt::format("{} ({})", name, var->category())); - combo->addItem(item_text, QString::fromStdString(name)); + SafeQString(fmt::format("{} ({})", name, var->category())); + combo->addItem(item_text, SafeQString(name)); } } @@ -632,7 +634,7 @@ void GameConfigDialogQt::OnAddOverrideClicked() { return; } - selected_var = (*cvar::ConfigVars)[var_name.toStdString()]; + selected_var = (*cvar::ConfigVars)[SafeStdString(var_name)]; if (!selected_var) { return; } @@ -678,7 +680,7 @@ void GameConfigDialogQt::OnAddOverrideClicked() { QString value; if (current_editor) { - value = QString::fromStdString(GetEditorValue(current_editor)); + value = SafeQString(GetEditorValue(current_editor)); } if (var_name.isEmpty()) { @@ -710,7 +712,7 @@ void GameConfigDialogQt::OnAddOverrideClicked() { // We need to create a new one since the dialog's editor will be destroyed if (selected_var) { QWidget* value_editor = - CreateEditorForCvar(selected_var, value.toStdString()); + CreateEditorForCvar(selected_var, SafeStdString(value)); overrides_table_->setCellWidget(row, 1, value_editor); } @@ -784,7 +786,7 @@ void GameConfigDialogQt::LoadRecommendedSettings() { if (!parse_result) { QMessageBox::warning( this, "Error Parsing Settings", - QString::fromStdString( + SafeQString( fmt::format("Failed to parse recommended settings: {} at offset {}", rapidjson::GetParseError_En(parse_result.Code()), parse_result.Offset()))); @@ -827,7 +829,7 @@ void GameConfigDialogQt::LoadRecommendedSettings() { has_unsaved_changes_ = true; QMessageBox::information( this, "Settings Loaded", - QString::fromStdString( + SafeQString( fmt::format("Loaded {} recommended setting{} for this game.\n\n" "Don't forget to save your changes!", settings_applied, settings_applied == 1 ? "" : "s"))); @@ -855,13 +857,13 @@ bool GameConfigDialogQt::ApplyRecommendedSetting(const std::string& var_name, // Check if this setting is already in the table for (int row = 0; row < overrides_table_->rowCount(); ++row) { auto* item = overrides_table_->item(row, 0); - if (item && item->text().toStdString() == var_name) { + if (item && SafeStdString(item->text()) == var_name) { // Update the existing entry QWidget* value_widget = overrides_table_->cellWidget(row, 1); if (value_widget) { // Update the widget with the new value if (auto* combo = qobject_cast(value_widget)) { - int index = combo->findText(QString::fromStdString(value)); + int index = combo->findText(SafeQString(value)); if (index >= 0) { combo->setCurrentIndex(index); } @@ -872,12 +874,12 @@ bool GameConfigDialogQt::ApplyRecommendedSetting(const std::string& var_name, XELOGW("Failed to parse integer value for {}: {}", var_name, value); } } else if (auto* line_edit = qobject_cast(value_widget)) { - line_edit->setText(QString::fromStdString(value)); + line_edit->setText(SafeQString(value)); } else if (auto* container = qobject_cast(value_widget)) { // For path containers, find the QLineEdit child auto* line_edit = container->findChild(); if (line_edit) { - line_edit->setText(QString::fromStdString(value)); + line_edit->setText(SafeQString(value)); } } } @@ -890,7 +892,7 @@ bool GameConfigDialogQt::ApplyRecommendedSetting(const std::string& var_name, overrides_table_->insertRow(row); // Column 0: Variable name (non-editable text) - auto* name_item = new QTableWidgetItem(QString::fromStdString(var_name)); + auto* name_item = new QTableWidgetItem(SafeQString(var_name)); name_item->setFlags(name_item->flags() & ~Qt::ItemIsEditable); overrides_table_->setItem(row, 0, name_item); diff --git a/src/xenia/ui/game_list_dialog_qt.cc b/src/xenia/ui/game_list_dialog_qt.cc index 28dbc5985..4a62ded40 100644 --- a/src/xenia/ui/game_list_dialog_qt.cc +++ b/src/xenia/ui/game_list_dialog_qt.cc @@ -48,10 +48,14 @@ #include "xenia/ui/game_config_dialog_qt.h" #include "xenia/ui/profile_dialog_qt.h" #include "xenia/ui/profile_editor_dialog_qt.h" +#include "xenia/ui/qt_util.h" namespace xe { namespace app { +using xe::ui::SafeQString; +using xe::ui::SafeStdString; + GameListDialogQt::GameListDialogQt(QWidget* parent, EmulatorWindow* emulator_window) : QWidget(parent), emulator_window_(emulator_window) { @@ -597,9 +601,8 @@ void GameListDialogQt::PopulateTable() { // Apply filter if (!filter_text.isEmpty()) { - QString title = QString::fromStdString(entry.title_name).toLower(); - QString path = - QString::fromStdString(entry.path_to_file.string()).toLower(); + QString title = SafeQString(entry.title_name).toLower(); + QString path = SafeQString(entry.path_to_file.string()).toLower(); if (!title.contains(filter_text) && !path.contains(filter_text)) { continue; @@ -661,9 +664,20 @@ void GameListDialogQt::PopulateTable() { bool file_corrupted = entry.title_name.empty(); // Title - large font - QString display_title = file_corrupted - ? QString("File Corrupted") - : QString::fromStdString(entry.title_name); + QString display_title; + if (file_corrupted) { + display_title = QString("File Corrupted"); + } else { + // Use fromUtf8 with error handling for potentially invalid data + display_title = + QString::fromUtf8(entry.title_name.c_str(), + static_cast(entry.title_name.size())); + // If conversion failed or resulted in empty string, mark as corrupted + if (display_title.isEmpty()) { + display_title = QString("File Corrupted"); + file_corrupted = true; + } + } auto* title_label = new QLabel(display_title); title_label->setAlignment(Qt::AlignVCenter | Qt::AlignLeft); title_label->setAttribute(Qt::WA_TransparentForMouseEvents); @@ -678,8 +692,7 @@ void GameListDialogQt::PopulateTable() { // Path - smaller font (only show if path is available) if (!entry.path_to_file.empty()) { - auto* path_label = - new QLabel(QString::fromStdString(entry.path_to_file.string())); + auto* path_label = new QLabel(SafeQString(entry.path_to_file.string())); path_label->setAlignment(Qt::AlignVCenter | Qt::AlignLeft); path_label->setAttribute(Qt::WA_TransparentForMouseEvents); QFont path_font = path_label->font(); @@ -721,8 +734,8 @@ void GameListDialogQt::PopulateTable() { row_layout->addWidget(title_container, 1); // Stretch factor // Last played - auto* last_played_label = new QLabel( - QString::fromStdString(FormatLastPlayed(entry.last_run_time))); + auto* last_played_label = + new QLabel(SafeQString(FormatLastPlayed(entry.last_run_time))); last_played_label->setAlignment(Qt::AlignVCenter | Qt::AlignRight); last_played_label->setMinimumWidth(200); last_played_label->setAttribute(Qt::WA_TransparentForMouseEvents); @@ -734,7 +747,7 @@ void GameListDialogQt::PopulateTable() { // Store the path and title_id in the row for later retrieval table_widget_->setItem(row, 0, new QTableWidgetItem()); table_widget_->item(row, 0)->setData( - Qt::UserRole, QString::fromStdString(entry.path_to_file.string())); + Qt::UserRole, SafeQString(entry.path_to_file.string())); table_widget_->item(row, 0)->setData(Qt::UserRole + 1, entry.title_id); } } @@ -766,7 +779,7 @@ void GameListDialogQt::OnGameDoubleClicked(int row, int column) { return; } - std::filesystem::path path = path_str.toStdString(); + std::filesystem::path path = SafeStdString(path_str); LaunchGame(path, title_id); } @@ -784,7 +797,7 @@ void GameListDialogQt::OnGameRightClicked(const QPoint& pos) { QString path_str = item->data(Qt::UserRole).toString(); uint32_t title_id = item->data(Qt::UserRole + 1).toUInt(); - std::filesystem::path path = path_str.toStdString(); + std::filesystem::path path = SafeStdString(path_str); bool has_path = !path_str.isEmpty(); // Get profile manager @@ -872,7 +885,7 @@ void GameListDialogQt::OnGameRightClicked(const QPoint& pos) { QString title_name; for (const auto& entry : game_entries_) { if (entry.title_id == title_id) { - title_name = QString::fromStdString(entry.title_name); + title_name = SafeQString(entry.title_name); break; } } @@ -938,16 +951,16 @@ void GameListDialogQt::OnGameRightClicked(const QPoint& pos) { QString title_name; for (const auto& entry : game_entries_) { if (entry.title_id == title_id) { - title_name = QString::fromStdString(entry.title_name); + title_name = SafeQString(entry.title_name); break; } } if (title_name.isEmpty()) { - title_name = QString::fromStdString(fmt::format("{:08X}", title_id)); + title_name = SafeQString(fmt::format("{:08X}", title_id)); } auto* dialog = new GameConfigDialogQt(this, emulator_window_, title_id, - title_name.toStdString()); + SafeStdString(title_name)); dialog->exec(); delete dialog; } else if (selected == compatibility_canary_action && @@ -957,7 +970,7 @@ void GameListDialogQt::OnGameRightClicked(const QPoint& pos) { "https://github.com/xenia-canary/game-compatibility/issues"; const std::string url = fmt::format("{}?q=is%3Aissue+is%3Aopen+{:08X}", base_url, title_id); - QDesktopServices::openUrl(QUrl(QString::fromStdString(url))); + QDesktopServices::openUrl(QUrl(SafeQString(url))); } else if (selected == compatibility_master_action && compatibility_master_action) { // Open Master game compatibility page with this title ID @@ -965,7 +978,7 @@ void GameListDialogQt::OnGameRightClicked(const QPoint& pos) { "https://github.com/xenia-project/game-compatibility/issues"; const std::string url = fmt::format("{}?q=is%3Aissue+is%3Aopen+{:08X}", base_url, title_id); - QDesktopServices::openUrl(QUrl(QString::fromStdString(url))); + QDesktopServices::openUrl(QUrl(SafeQString(url))); } else if (selected == remove_action) { RemoveTitleFromDashboard(title_id); } @@ -1139,7 +1152,7 @@ void GameListDialogQt::OnSelectionChanged() { QString path_str = item->data(Qt::UserRole).toString(); uint32_t title_id = item->data(Qt::UserRole + 1).toUInt(); if (!path_str.isEmpty()) { - selected_game_path_ = path_str.toStdString(); + selected_game_path_ = SafeStdString(path_str); selected_game_title_id_ = title_id; UpdatePlayButtonState(); return; @@ -1226,8 +1239,7 @@ void GameListDialogQt::UpdateProfileButtonState() { auto accounts = profile_manager->GetAccounts(); auto account_it = accounts->find(profile->xuid()); if (account_it != accounts->end()) { - QString gamertag = - QString::fromStdString(account_it->second.GetGamertagString()); + QString gamertag = SafeQString(account_it->second.GetGamertagString()); profile_label_->setText(gamertag); profile_question_label_->setVisible( false); // Hide question mark when logged in diff --git a/src/xenia/ui/postprocessing_dialog_qt.cc b/src/xenia/ui/postprocessing_dialog_qt.cc index 0360bbca8..b87fef0d4 100644 --- a/src/xenia/ui/postprocessing_dialog_qt.cc +++ b/src/xenia/ui/postprocessing_dialog_qt.cc @@ -21,6 +21,7 @@ #include "xenia/app/emulator_window.h" #include "xenia/base/cvar.h" #include "xenia/gpu/graphics_system.h" +#include "xenia/ui/qt_util.h" DECLARE_bool(postprocess_dither); DECLARE_double(postprocess_ffx_cas_additional_sharpness); @@ -31,6 +32,8 @@ DECLARE_string(postprocess_scaling_and_sharpening); namespace xe { namespace app { +using xe::ui::SafeQString; + PostProcessingDialogQt::PostProcessingDialogQt(QWidget* parent, EmulatorWindow* emulator_window) : QDialog(parent), emulator_window_(emulator_window) { @@ -446,8 +449,8 @@ void PostProcessingDialogQt::OnFsrSharpnessChanged(int value) { presenter->SetGuestOutputPaintConfigFromUIThread(config); // Update label - fsr_sharpness_value_label_->setText(QString::fromStdString( - fmt::format("{} %", static_cast(fsr_sharpness * 100)))); + fsr_sharpness_value_label_->setText( + SafeQString(fmt::format("{} %", static_cast(fsr_sharpness * 100)))); // Update cvar emulator_window_->UpdateFsrSharpnessCvar(fsr_sharpness); @@ -477,8 +480,8 @@ void PostProcessingDialogQt::OnCasSharpnessChanged(int value) { presenter->SetGuestOutputPaintConfigFromUIThread(config); // Update label - cas_sharpness_value_label_->setText(QString::fromStdString( - fmt::format("{} %", static_cast(cas_sharpness * 100)))); + cas_sharpness_value_label_->setText( + SafeQString(fmt::format("{} %", static_cast(cas_sharpness * 100)))); // Update cvar emulator_window_->UpdateCasSharpnessCvar(cas_sharpness); diff --git a/src/xenia/ui/profile_dialog_qt.cc b/src/xenia/ui/profile_dialog_qt.cc index 1008a2e3d..54d8fe4dd 100644 --- a/src/xenia/ui/profile_dialog_qt.cc +++ b/src/xenia/ui/profile_dialog_qt.cc @@ -25,11 +25,15 @@ #include "xenia/kernel/xam/ui/title_info_ui.h" #include "xenia/kernel/xam/xam_state.h" #include "xenia/ui/profile_editor_dialog_qt.h" +#include "xenia/ui/qt_util.h" #include "xenia/ui/window_qt.h" namespace xe { namespace app { +using xe::ui::SafeQString; +using xe::ui::SafeStdString; + ProfileDialogQt::ProfileDialogQt(QWidget* parent, EmulatorWindow* emulator_window) : QDialog(parent), emulator_window_(emulator_window) { @@ -206,7 +210,7 @@ void ProfileDialogQt::PopulateProfileList() { const uint8_t user_index = profile_manager->GetUserIndexAssignedToProfile(xuid); - QString gamertag = QString::fromStdString(account.GetGamertagString()); + QString gamertag = SafeQString(account.GetGamertagString()); QString status = (user_index == XUserIndexAny) ? " (Not logged in)" : fmt::format(" (Slot {})", user_index + 1).c_str(); @@ -327,8 +331,8 @@ void ProfileDialogQt::OnProfileContextMenu(const QPoint& pos) { // Login to slot submenu QMenu* login_slot_menu = context_menu.addMenu("Login to slot:"); for (uint8_t i = 1; i <= XUserMaxUserCount; i++) { - QAction* slot_action = login_slot_menu->addAction( - QString::fromStdString(fmt::format("slot {}", i))); + QAction* slot_action = + login_slot_menu->addAction(SafeQString(fmt::format("slot {}", i))); connect(slot_action, &QAction::triggered, [=, this]() { profile_manager->Login(xuid, i - 1); RefreshProfiles(); @@ -382,8 +386,7 @@ void ProfileDialogQt::OnProfileContextMenu(const QPoint& pos) { return; } - QString gamertag = - QString::fromStdString(profile_it->second.GetGamertagString()); + QString gamertag = SafeQString(profile_it->second.GetGamertagString()); QMessageBox::StandardButton reply = QMessageBox::question( this, "Delete Profile", @@ -442,7 +445,7 @@ void ProfileDialogQt::OnCreateProfileClicked() { } // Create the profile - std::string gamertag_string = gamertag.toStdString(); + std::string gamertag_string = SafeStdString(gamertag); bool autologin = (profile_manager->GetAccountCount() == 0); if (profile_manager->CreateProfile(gamertag_string, autologin, false)) { diff --git a/src/xenia/ui/profile_editor_dialog_qt.cc b/src/xenia/ui/profile_editor_dialog_qt.cc index bb90fef0c..c0ff82f71 100644 --- a/src/xenia/ui/profile_editor_dialog_qt.cc +++ b/src/xenia/ui/profile_editor_dialog_qt.cc @@ -30,11 +30,14 @@ #include "xenia/kernel/xam/user_settings.h" #include "xenia/kernel/xam/user_tracker.h" #include "xenia/kernel/xam/xam_state.h" +#include "xenia/ui/qt_util.h" namespace xe { namespace app { using namespace kernel::xam; +using xe::ui::SafeQString; +using xe::ui::SafeStdString; // Language names static const char* kLanguageNames[] = {nullptr, @@ -361,7 +364,7 @@ void ProfileEditorDialogQt::SetupUI() { const auto profile = xam_state ? xam_state->GetUserProfile(xuid_) : nullptr; setWindowTitle(QString("Gamercard Editor - %1") - .arg(QString::fromStdString(current_data_.gamertag))); + .arg(SafeQString(current_data_.gamertag))); setModal(false); // Non-modal like ProfileDialogQt setAttribute(Qt::WA_DeleteOnClose); setMinimumSize(900, 700); @@ -416,8 +419,7 @@ void ProfileEditorDialogQt::SetupUI() { // Gamertag profile_layout->addWidget(new QLabel("Gamertag:"), row, 0); - gamertag_edit_ = - new QLineEdit(QString::fromStdString(current_data_.gamertag)); + gamertag_edit_ = new QLineEdit(SafeQString(current_data_.gamertag)); gamertag_edit_->setMaxLength(15); connect(gamertag_edit_, &QLineEdit::textChanged, this, &ProfileEditorDialogQt::UpdateGamertagValidation); @@ -430,24 +432,21 @@ void ProfileEditorDialogQt::SetupUI() { // Gamer Name profile_layout->addWidget(new QLabel("Gamer Name:"), row, 0); - gamer_name_edit_ = - new QLineEdit(QString::fromStdString(current_data_.gamer_name)); + gamer_name_edit_ = new QLineEdit(SafeQString(current_data_.gamer_name)); gamer_name_edit_->setMaxLength(130); // 0x104 bytes = 130 UTF-16 characters gamer_name_edit_->setEnabled(profile != nullptr); profile_layout->addWidget(gamer_name_edit_, row++, 1); // Gamer Motto profile_layout->addWidget(new QLabel("Gamer Motto:"), row, 0); - gamer_motto_edit_ = - new QLineEdit(QString::fromStdString(current_data_.gamer_motto)); + gamer_motto_edit_ = new QLineEdit(SafeQString(current_data_.gamer_motto)); gamer_motto_edit_->setMaxLength(22); // 0x2C bytes = 22 UTF-16 characters gamer_motto_edit_->setEnabled(profile != nullptr); profile_layout->addWidget(gamer_motto_edit_, row++, 1); // Gamer Bio profile_layout->addWidget(new QLabel("Gamer Bio:"), row, 0); - gamer_bio_edit_ = - new QTextEdit(QString::fromStdString(current_data_.gamer_bio)); + gamer_bio_edit_ = new QTextEdit(SafeQString(current_data_.gamer_bio)); gamer_bio_edit_->setEnabled(profile != nullptr); gamer_bio_edit_->setMaximumHeight(100); // Limit bio to 500 characters (0x3E8 bytes = 500 UTF-16 characters) @@ -503,15 +502,13 @@ void ProfileEditorDialogQt::SetupUI() { // Online XUID (read-only) online_layout->addWidget(new QLabel("Online XUID:"), row, 0); - online_xuid_edit_ = - new QLineEdit(QString::fromStdString(current_data_.online_xuid)); + online_xuid_edit_ = new QLineEdit(SafeQString(current_data_.online_xuid)); online_xuid_edit_->setReadOnly(true); online_layout->addWidget(online_xuid_edit_, row++, 1); // Online Domain (read-only) online_layout->addWidget(new QLabel("Online Domain:"), row, 0); - online_domain_edit_ = - new QLineEdit(QString::fromStdString(current_data_.online_domain)); + online_domain_edit_ = new QLineEdit(SafeQString(current_data_.online_domain)); online_domain_edit_->setReadOnly(true); online_layout->addWidget(online_domain_edit_, row++, 1); @@ -731,7 +728,7 @@ void ProfileEditorDialogQt::LoadProfileIcon() { } bool ProfileEditorDialogQt::ValidateGamertagInput(const QString& text) { - std::string gamertag = text.toStdString(); + std::string gamertag = SafeStdString(text); return ProfileManager::IsGamertagValid(gamertag); } @@ -762,7 +759,7 @@ void ProfileEditorDialogQt::OnChangeIconClicked() { return; } - std::filesystem::path path = file_path.toStdString(); + std::filesystem::path path = SafeStdString(file_path); if (!IsFilePngImage(path)) { QMessageBox::warning(this, "Invalid File", @@ -820,7 +817,7 @@ void ProfileEditorDialogQt::SaveProfileData() { auto account = account_original; // Update gamertag - std::string gamertag_str = gamertag_edit_->text().toStdString(); + std::string gamertag_str = SafeStdString(gamertag_edit_->text()); std::u16string gamertag = xe::to_utf16(gamertag_str); string_util::copy_truncating(account.gamertag, gamertag, std::size(account.gamertag)); @@ -864,7 +861,7 @@ void ProfileEditorDialogQt::SaveProfileData() { size_t max_utf16_chars) { // Convert QString to std::u16string using xe::to_utf16 for proper // conversion - std::string utf8_text = text.toStdString(); + std::string utf8_text = SafeStdString(text); std::u16string new_value = xe::to_utf16(utf8_text); // Truncate if exceeds max length (in UTF-16 characters) diff --git a/src/xenia/ui/qt_util.h b/src/xenia/ui/qt_util.h new file mode 100644 index 000000000..7db117e83 --- /dev/null +++ b/src/xenia/ui/qt_util.h @@ -0,0 +1,35 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2025 Xenia Canary. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_UI_QT_UTIL_H_ +#define XENIA_UI_QT_UTIL_H_ + +#include +#include + +namespace xe { +namespace ui { + +// Safely convert std::string to QString, handling invalid UTF-8 data +// Uses QString::fromUtf8 which is more robust than fromStdString +inline QString SafeQString(const std::string& str) { + return QString::fromUtf8(str.c_str(), static_cast(str.size())); +} + +// Safely convert QString to std::string, handling invalid UTF-8 data +// Uses QString::toUtf8 which is more robust than toStdString +inline std::string SafeStdString(const QString& qstr) { + QByteArray utf8 = qstr.toUtf8(); + return std::string(utf8.constData(), utf8.size()); +} + +} // namespace ui +} // namespace xe + +#endif // XENIA_UI_QT_UTIL_H_ diff --git a/src/xenia/ui/xmp_dialog_qt.cc b/src/xenia/ui/xmp_dialog_qt.cc index 9c079b743..c14c8b36e 100644 --- a/src/xenia/ui/xmp_dialog_qt.cc +++ b/src/xenia/ui/xmp_dialog_qt.cc @@ -16,10 +16,13 @@ #include "third_party/fmt/include/fmt/format.h" #include "xenia/app/emulator_window.h" +#include "xenia/ui/qt_util.h" namespace xe { namespace app { +using xe::ui::SafeQString; + XmpDialogQt::XmpDialogQt(QWidget* parent, EmulatorWindow* emulator_window) : QDialog(parent), emulator_window_(emulator_window) { SetupUI(); @@ -246,8 +249,8 @@ void XmpDialogQt::UpdatePlayerState() { float volume = audio_player->GetVolume()->load(); volume_slider_->blockSignals(true); volume_slider_->setValue(static_cast(volume * 100)); - volume_value_label_->setText(QString::fromStdString( - fmt::format("{}%", static_cast(volume * 100)))); + volume_value_label_->setText( + SafeQString(fmt::format("{}%", static_cast(volume * 100)))); volume_slider_->blockSignals(false); } @@ -287,8 +290,7 @@ void XmpDialogQt::OnVolumeChanged(int value) { audio_player->SetVolume(volume); // Update label - volume_value_label_->setText( - QString::fromStdString(fmt::format("{}%", value))); + volume_value_label_->setText(SafeQString(fmt::format("{}%", value))); } } // namespace app