diff --git a/pcsx2-qt/QtUtils.cpp b/pcsx2-qt/QtUtils.cpp index 053282a36c..772b2995ed 100644 --- a/pcsx2-qt/QtUtils.cpp +++ b/pcsx2-qt/QtUtils.cpp @@ -4,6 +4,7 @@ #include "QtUtils.h" #include +#include #include #include #include @@ -15,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -27,6 +29,8 @@ #include #include +#include + #ifdef Q_OS_LINUX #include #endif @@ -37,11 +41,22 @@ #include "common/CocoaTools.h" #include "common/Console.h" +#include "common/FileSystem.h" +#include "common/Path.h" +#include "common/StringUtil.h" +#include "pcsx2/Config.h" #include "QtHost.h" #if defined(_WIN32) #include "common/RedtapeWindows.h" +#include "common/RedtapeWilCom.h" #include +#include +#include +#else +#include +#include +#include #endif namespace QtUtils @@ -549,4 +564,430 @@ namespace QtUtils const QString flag_path = QStringLiteral("%1/icons/flags/%2.svg").arg(QtHost::GetResourcesBasePath()).arg(country_code.toLower()); return QIcon(flag_path); } + + void CreateShortcut(QWidget* parent, const std::string& name, const std::string& game_path, + std::vector passed_cli_args, const std::string& custom_args, + const std::string& icon_path, bool is_desktop) + { + const auto tr_msg = [](const char* str) { + return QCoreApplication::translate("ShortcutCreationDialog", str); + }; + +#if defined(_WIN32) + if (name.empty()) + { + Console.Error("Cannot create shortcuts without a name."); + return; + } + + // Sanitize filename + const std::string clean_name = Path::SanitizeFileName(name).c_str(); + std::string clean_path = Path::ToNativePath(Path::RealPath(game_path)).c_str(); + if (!Path::IsValidFileName(clean_name)) + { + QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("Filename contains illegal character."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); + return; + } + + // Get path to Desktop or per-user Start Menu\Programs directory + // https://superuser.com/questions/1489874/how-can-i-get-the-real-path-of-desktop-in-windows-explorer/1789849#1789849 + // https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shgetknownfolderpath + // https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid + std::string link_file; + if (wil::unique_cotaskmem_string directory; SUCCEEDED(SHGetKnownFolderPath(is_desktop ? FOLDERID_Desktop : FOLDERID_Programs, 0, NULL, &directory))) + { + std::string directory_utf8 = StringUtil::WideStringToUTF8String(directory.get()); + + if (is_desktop) + link_file = Path::ToNativePath(fmt::format("{}/{}.lnk", directory_utf8, clean_name)); + else + { + const std::string pcsx2_start_menu_dir = Path::ToNativePath(fmt::format("{}/PCSX2", directory_utf8)); + if (!FileSystem::EnsureDirectoryExists(pcsx2_start_menu_dir.c_str(), false)) + { + QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("Could not create start menu directory."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); + return; + } + + link_file = Path::ToNativePath(fmt::format("{}/{}.lnk", pcsx2_start_menu_dir, clean_name)); + } + } + else + { + QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), is_desktop ? tr_msg("'Desktop' directory not found") : tr_msg("User's 'Start Menu\\Programs' directory not found"), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); + return; + } + + // Check if the same shortcut already exists + if (FileSystem::FileExists(link_file.c_str())) + { + QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("A shortcut with the same name already exists."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); + return; + } + + // Shortcut CmdLine Args + bool lossless = true; + for (std::string& arg : passed_cli_args) + lossless &= EscapeShortcutCommandLine(&arg); + + if (!lossless) + { + QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("File path contains invalid character(s)."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); + return; + } + + EscapeShortcutCommandLine(&clean_path); + std::string combined_args = StringUtil::JoinString(passed_cli_args.begin(), passed_cli_args.end(), " "); + std::string final_args = fmt::format("{} {} -- {}", combined_args, custom_args, clean_path); + + Console.WriteLnFmt("Creating a shortcut '{}' with arguments '{}'", link_file, final_args); + + const auto str_error = [](HRESULT hr) -> std::string { + _com_error err(hr); + const TCHAR* errMsg = err.ErrorMessage(); + return fmt::format("{} [{}]", StringUtil::WideStringToUTF8String(errMsg), hr); + }; + + // Construct the shortcut + // https://stackoverflow.com/questions/3906974/how-to-programmatically-create-a-shortcut-using-win32 + HRESULT res = CoInitialize(NULL); + if (FAILED(res)) + { + Console.ErrorFmt("Failed to create shortcut: CoInitialize failed ({})", str_error(res)); + QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("CoInitialize failed (%1)").arg(QString::fromStdString(str_error(res))), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); + return; + } + + wil::unique_couninitialize_call co_cleanup; + + const auto report_error = [&](const QString& reason) { + Console.ErrorFmt("Failed to create shortcut: {}", reason.toStdString()); + QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), reason, QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); + }; + + wil::com_ptr_nothrow pShellLink = wil::CoCreateInstanceNoThrow(CLSID_ShellLink); + wil::com_ptr_nothrow pPersistFile; + + if (!pShellLink) + { + report_error(tr_msg("CoCreateInstance failed")); + return; + } + + // Set path to the executable + const std::wstring target_file = StringUtil::UTF8StringToWideString(FileSystem::GetProgramPath()); + res = pShellLink->SetPath(target_file.c_str()); + if (FAILED(res)) + { + report_error(tr_msg("SetPath failed (%1)").arg(QString::fromStdString(str_error(res)))); + return; + } + + // Set the working directory + const std::wstring working_dir = StringUtil::UTF8StringToWideString(FileSystem::GetWorkingDirectory()); + res = pShellLink->SetWorkingDirectory(working_dir.c_str()); + if (FAILED(res)) + { + report_error(tr_msg("SetWorkingDirectory failed (%1)").arg(QString::fromStdString(str_error(res)))); + return; + } + + // Set the launch arguments + if (!final_args.empty()) + { + const std::wstring target_cli_args = StringUtil::UTF8StringToWideString(final_args); + res = pShellLink->SetArguments(target_cli_args.c_str()); + if (FAILED(res)) + { + report_error(tr_msg("SetArguments failed (%1)").arg(QString::fromStdString(str_error(res)))); + return; + } + } + + // Set the icon + std::string final_icon_path; + if (!icon_path.empty()) + { + final_icon_path = Path::ToNativePath(icon_path); + if (!FileSystem::FileExists(final_icon_path.c_str())) + { + QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("The selected icon file does not exist."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); + return; + } + } + else + { + final_icon_path = Path::ToNativePath(Path::Combine(Path::GetDirectory(FileSystem::GetProgramPath()), "resources/icons/AppIconLarge.ico")); + } + + const std::wstring w_icon_path = StringUtil::UTF8StringToWideString(final_icon_path); + res = pShellLink->SetIconLocation(w_icon_path.c_str(), 0); + if (FAILED(res)) + { + report_error(tr_msg("SetIconLocation failed (%1)").arg(QString::fromStdString(str_error(res)))); + return; + } + + // Use the IPersistFile object to save the shell link + res = pShellLink.query_to(&pPersistFile); + if (FAILED(res)) + { + report_error(tr_msg("QueryInterface failed (%1)").arg(QString::fromStdString(str_error(res)))); + return; + } + + // Save shortcut link to disk + const std::wstring w_link_file = StringUtil::UTF8StringToWideString(link_file); + res = pPersistFile->Save(w_link_file.c_str(), TRUE); + if (FAILED(res)) + { + report_error(tr_msg("Failed to save the shortcut (%1)").arg(QString::fromStdString(str_error(res)))); + return; + } + +#else + + if (name.empty()) + { + QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("Cannot create a shortcut without a title."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); + return; + } + + bool is_flatpak = (std::getenv("container")); + + // Sanitize filename and game path + const std::string clean_name = Path::SanitizeFileName(name); + std::string clean_path = Path::Canonicalize(Path::RealPath(game_path)); + if (!Path::IsValidFileName(clean_name)) + { + QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("Filename contains illegal character."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); + return; + } + + // Find the executable path + std::string executable_path = FileSystem::GetPackagePath(); + if (executable_path.empty()) + { + QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("Executable path is empty."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); + return; + } + + // Find home directory + std::string link_path; + const char* home = std::getenv("HOME"); + const char* xdg_desktop_dir = std::getenv("XDG_DESKTOP_DIR"); + const char* xdg_data_home = std::getenv("XDG_DATA_HOME"); + if (home) + { + if (is_desktop) + { + if (xdg_desktop_dir) + link_path = fmt::format("{}/{}.desktop", xdg_desktop_dir, clean_name); + else + link_path = fmt::format("{}/Desktop/{}.desktop", home, clean_name); + } + else + { + if (xdg_data_home) + link_path = fmt::format("{}/applications/{}.desktop", xdg_data_home, clean_name); + else + link_path = fmt::format("{}/.local/share/applications/{}.desktop", home, clean_name); + } + } + else + { + QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("Path to the Home directory is empty."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); + return; + } + + std::string icon_name; + if (!icon_path.empty()) + { + if (!FileSystem::FileExists(icon_path.c_str())) + { + QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("The selected icon file does not exist."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); + return; + } + icon_name = icon_path; + } + else + { + // Copy PCSX2 icon + std::string icon_dest; + if (xdg_data_home) + icon_dest = fmt::format("{}/icons/hicolor/512x512/apps/", xdg_data_home); + else + icon_dest = fmt::format("{}/.local/share/icons/hicolor/512x512/apps/", home); + + if (is_flatpak) // Flatpak + { + executable_path = "flatpak run net.pcsx2.PCSX2"; + icon_name = "net.pcsx2.PCSX2"; + } + else + { + icon_name = "PCSX2"; + std::string icon_path_dest = fmt::format("{}/{}.png", icon_dest, icon_name).c_str(); + if (FileSystem::EnsureDirectoryExists(icon_dest.c_str(), true)) + if (!FileSystem::FileExists(icon_path_dest.c_str())) + FileSystem::CopyFilePath(Path::Combine(EmuFolders::Resources, "icons/AppIconLarge.png").c_str(), icon_path_dest.c_str(), false); + } + } + + // Shortcut CmdLine Args + bool lossless = true; + for (std::string& arg : passed_cli_args) + lossless &= EscapeShortcutCommandLine(&arg); + + if (!lossless) + { + QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("File path contains invalid character(s)."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); + return; + } + + std::string cmdline = StringUtil::JoinString(passed_cli_args.begin(), passed_cli_args.end(), " "); + + // Further string sanitization + if (!is_flatpak) + EscapeShortcutCommandLine(&executable_path); + EscapeShortcutCommandLine(&clean_path); + + // Assembling the .desktop file + std::string final_args; + final_args = fmt::format("{} {} {} -- {}", executable_path, cmdline, custom_args, clean_path); + std::string file_content = + "[Desktop Entry]\n" + "Encoding=UTF-8\n" + "Version=1.0\n" + "Type=Application\n" + "Terminal=false\n" + "StartupWMClass=PCSX2\n" + "Exec=" + + final_args + "\n" + + "Name=" + + clean_name + "\n" + + "Icon=" + + icon_name + "\n" + + "Categories=Game;Emulator;\n"; + std::string_view sv(file_content); + + // Prompt user for shortcut saving destination + QString final_path(QStringLiteral("%1").arg(QString::fromStdString(link_path))); + const QString filter(tr_msg("Desktop Shortcut Files (*.desktop)")); + + final_path = QDir::toNativeSeparators(QFileDialog::getSaveFileName(parent, tr_msg("Select Shortcut Save Destination"), final_path, filter)); + + if (final_path.isEmpty()) + return; + + // Write to .desktop file + if (!FileSystem::WriteStringToFile(final_path.toStdString().c_str(), sv)) + { + QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("Failed to create .desktop file"), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); + return; + } + + if (chmod(final_path.toStdString().c_str(), S_IRWXU) != 0) // enables user to execute file + Console.ErrorFmt("Failed to change file permissions for .desktop file: {} ({})", strerror(errno), errno); +#endif + } + + bool EscapeShortcutCommandLine(std::string* arg) + { +#ifdef _WIN32 + if (!arg->empty() && arg->find_first_of(" \t\n\v\"") == std::string::npos) + return true; + + std::string temp; + temp.reserve(arg->length() + 10); + temp += '"'; + + for (auto it = arg->begin();; ++it) + { + int backslash_count = 0; + while (it != arg->end() && *it == '\\') + { + ++it; + ++backslash_count; + } + + if (it == arg->end()) + { + temp.append(backslash_count * 2, '\\'); + break; + } + + if (*it == '"') + { + temp.append(backslash_count * 2 + 1, '\\'); + temp += '"'; + } + else + { + temp.append(backslash_count, '\\'); + temp += *it; + } + } + + temp += '"'; + *arg = std::move(temp); + return true; +#else + const char* carg = arg->c_str(); + const char* cend = carg + arg->size(); + const char* RESERVED_CHARS = " \t\n\\\"'\\\\><~|%&;$*?#()`" + "\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0d\x0e\x0f" + "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x7f"; + const char* next = carg + std::strcspn(carg, RESERVED_CHARS); + + if (next == cend) + return true; // No escaping needed, don't modify + + bool lossless = true; + std::string temp = "\""; + const char* NOT_VALID_IN_QUOTE = "%`$\"\\\n" + "\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0d\x0e\x0f" + "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x7f"; + + while (true) + { + next = carg + std::strcspn(carg, NOT_VALID_IN_QUOTE); + temp.append(carg, next); + carg = next; + + if (carg == cend) + break; + + switch (*carg) + { + case '"': + case '`': + temp.push_back('\\'); + temp.push_back(*carg); + break; + case '\\': + temp.append("\\\\\\\\"); + break; + case '$': + temp.push_back('\\'); + temp.push_back('\\'); + temp.push_back(*carg); + break; + case '%': + temp.push_back('%'); + temp.push_back(*carg); + break; + default: + temp.push_back(' '); + lossless = false; + break; + } + ++carg; + } + + temp.push_back('"'); + *arg = std::move(temp); + return lossless; +#endif + } } // namespace QtUtils diff --git a/pcsx2-qt/QtUtils.h b/pcsx2-qt/QtUtils.h index bf2c629507..c5edb31089 100644 --- a/pcsx2-qt/QtUtils.h +++ b/pcsx2-qt/QtUtils.h @@ -17,9 +17,11 @@ #include #include #include +#include #include #include #include +#include #include "common/Console.h" @@ -209,4 +211,14 @@ namespace QtUtils /// Gets a flag icon for a given language code /// Returns an empty QIcon if no flag is available for the language QIcon GetFlagIconForLanguage(const QString& language_code); + + /// Creates a desktop or launcher (Start Menu / applications) shortcut for a game. + /// Shows error dialogs parented to `parent` when something goes wrong. + void CreateShortcut(QWidget* parent, const std::string& name, const std::string& game_path, + std::vector passed_cli_args, const std::string& custom_args, + const std::string& icon_path, bool is_desktop); + + /// Escapes the given string for use as a shortcut command line argument. + /// Returns whether the escaping operation was lossless. + bool EscapeShortcutCommandLine(std::string* arg); } // namespace QtUtils diff --git a/pcsx2-qt/ShortcutCreationDialog.cpp b/pcsx2-qt/ShortcutCreationDialog.cpp index db817c2449..e581812f6d 100644 --- a/pcsx2-qt/ShortcutCreationDialog.cpp +++ b/pcsx2-qt/ShortcutCreationDialog.cpp @@ -3,23 +3,14 @@ #include "ShortcutCreationDialog.h" #include "QtHost.h" -#include +#include "QtUtils.h" #include #include #include -#include "common/Console.h" -#include "common/FileSystem.h" #include "common/Path.h" #include "common/StringUtil.h" #include "VMManager.h" -#if defined(_WIN32) -#include "common/RedtapeWilCom.h" -#include -#include -#include -#endif - ShortcutCreationDialog::ShortcutCreationDialog(QWidget* parent, const QString& title, const QString& path) : QDialog(parent) , m_title(title) @@ -162,430 +153,10 @@ ShortcutCreationDialog::ShortcutCreationDialog(QWidget* parent, const QString& t std::string custom_args = m_ui.customArgsInput->text().toStdString(); std::string icon_path = m_ui.iconPath->text().toStdString(); - ShortcutCreationDialog::CreateShortcut(title.toStdString(), path.toStdString(), args, custom_args, icon_path, m_ui.shortcutDesktop->isChecked()); + QtUtils::CreateShortcut(this, title.toStdString(), path.toStdString(), std::move(args), custom_args, icon_path, m_ui.shortcutDesktop->isChecked()); accept(); }); } -void ShortcutCreationDialog::CreateShortcut(const std::string name, const std::string game_path, std::vector passed_cli_args, std::string custom_args, const std::string icon_path, bool is_desktop) -{ -#if defined(_WIN32) - if (name.empty()) - { - Console.Error("Cannot create shortcuts without a name."); - return; - } - - // Sanitize filename - const std::string clean_name = Path::SanitizeFileName(name).c_str(); - std::string clean_path = Path::ToNativePath(Path::RealPath(game_path)).c_str(); - if (!Path::IsValidFileName(clean_name)) - { - QMessageBox::critical(this, tr("Failed to create shortcut"), tr("Filename contains illegal character."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); - return; - } - - // Get path to Desktop or per-user Start Menu\Programs directory - // https://superuser.com/questions/1489874/how-can-i-get-the-real-path-of-desktop-in-windows-explorer/1789849#1789849 - // https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shgetknownfolderpath - // https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid - std::string link_file; - if (wil::unique_cotaskmem_string directory; SUCCEEDED(SHGetKnownFolderPath(is_desktop ? FOLDERID_Desktop : FOLDERID_Programs, 0, NULL, &directory))) - { - std::string directory_utf8 = StringUtil::WideStringToUTF8String(directory.get()); - - if (is_desktop) - link_file = Path::ToNativePath(fmt::format("{}/{}.lnk", directory_utf8, clean_name)); - else - { - const std::string pcsx2_start_menu_dir = Path::ToNativePath(fmt::format("{}/PCSX2", directory_utf8)); - if (!FileSystem::EnsureDirectoryExists(pcsx2_start_menu_dir.c_str(), false)) - { - QMessageBox::critical(this, tr("Failed to create shortcut"), tr("Could not create start menu directory."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); - return; - } - - link_file = Path::ToNativePath(fmt::format("{}/{}.lnk", pcsx2_start_menu_dir, clean_name)); - } - } - else - { - QMessageBox::critical(this, tr("Failed to create shortcut"), is_desktop ? tr("'Desktop' directory not found") : tr("User's 'Start Menu\\Programs' directory not found"), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); - return; - } - - // Check if the same shortcut already exists - if (FileSystem::FileExists(link_file.c_str())) - { - QMessageBox::critical(this, tr("Failed to create shortcut"), tr("A shortcut with the same name already exists."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); - return; - } - - // Shortcut CmdLine Args - bool lossless = true; - for (std::string& arg : passed_cli_args) - lossless &= ShortcutCreationDialog::EscapeShortcutCommandLine(&arg); - - if (!lossless) - { - QMessageBox::critical(this, tr("Failed to create shortcut"), tr("File path contains invalid character(s)."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); - return; - } - - ShortcutCreationDialog::EscapeShortcutCommandLine(&clean_path); - std::string combined_args = StringUtil::JoinString(passed_cli_args.begin(), passed_cli_args.end(), " "); - std::string final_args = fmt::format("{} {} -- {}", combined_args, custom_args, clean_path); - - Console.WriteLnFmt("Creating a shortcut '{}' with arguments '{}'", link_file, final_args); - - const auto str_error = [](HRESULT hr) -> std::string { - _com_error err(hr); - const TCHAR* errMsg = err.ErrorMessage(); - return fmt::format("{} [{}]", StringUtil::WideStringToUTF8String(errMsg), hr); - }; - - // Construct the shortcut - // https://stackoverflow.com/questions/3906974/how-to-programmatically-create-a-shortcut-using-win32 - HRESULT res = CoInitialize(NULL); - if (FAILED(res)) - { - Console.ErrorFmt("Failed to create shortcut: CoInitialize failed ({})", str_error(res)); - QMessageBox::critical(this, tr("Failed to create shortcut"), tr("CoInitialize failed (%1)").arg(str_error(res)), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); - return; - } - - wil::unique_couninitialize_call co_cleanup; - - const auto report_error = [&](const QString& reason) { - Console.ErrorFmt("Failed to create shortcut: {}", reason.toStdString()); - QMessageBox::critical(this, tr("Failed to create shortcut"), reason, QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); - }; - - wil::com_ptr_nothrow pShellLink = wil::CoCreateInstanceNoThrow(CLSID_ShellLink); - wil::com_ptr_nothrow pPersistFile; - - if (!pShellLink) - { - report_error(tr("CoCreateInstance failed")); - return; - } - - // Set path to the executable - const std::wstring target_file = StringUtil::UTF8StringToWideString(FileSystem::GetProgramPath()); - res = pShellLink->SetPath(target_file.c_str()); - if (FAILED(res)) - { - report_error(tr("SetPath failed (%1)").arg(str_error(res))); - return; - } - - // Set the working directory - const std::wstring working_dir = StringUtil::UTF8StringToWideString(FileSystem::GetWorkingDirectory()); - res = pShellLink->SetWorkingDirectory(working_dir.c_str()); - if (FAILED(res)) - { - report_error(tr("SetWorkingDirectory failed (%1)").arg(str_error(res))); - return; - } - - // Set the launch arguments - if (!final_args.empty()) - { - const std::wstring target_cli_args = StringUtil::UTF8StringToWideString(final_args); - res = pShellLink->SetArguments(target_cli_args.c_str()); - if (FAILED(res)) - { - report_error(tr("SetArguments failed (%1)").arg(str_error(res))); - return; - } - } - - // Set the icon - std::string final_icon_path; - if (!icon_path.empty()) - { - final_icon_path = Path::ToNativePath(icon_path); - if (!FileSystem::FileExists(final_icon_path.c_str())) - { - QMessageBox::critical(this, tr("Failed to create shortcut"), tr("The selected icon file does not exist."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); - return; - } - } - else - { - final_icon_path = Path::ToNativePath(Path::Combine(Path::GetDirectory(FileSystem::GetProgramPath()), "resources/icons/AppIconLarge.ico")); - } - - const std::wstring w_icon_path = StringUtil::UTF8StringToWideString(final_icon_path); - res = pShellLink->SetIconLocation(w_icon_path.c_str(), 0); - if (FAILED(res)) - { - report_error(tr("SetIconLocation failed (%1)").arg(str_error(res))); - return; - } - - // Use the IPersistFile object to save the shell link - res = pShellLink.query_to(&pPersistFile); - if (FAILED(res)) - { - report_error(tr("QueryInterface failed (%1)").arg(str_error(res))); - return; - } - - // Save shortcut link to disk - const std::wstring w_link_file = StringUtil::UTF8StringToWideString(link_file); - res = pPersistFile->Save(w_link_file.c_str(), TRUE); - if (FAILED(res)) - { - report_error(tr("Failed to save the shortcut (%1)").arg(str_error(res))); - return; - } - -#else - - if (name.empty()) - { - QMessageBox::critical(this, tr("Failed to create shortcut"), tr("Cannot create a shortcut without a title."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); - return; - } - - bool is_flatpak = (std::getenv("container")); - - // Sanitize filename and game path - const std::string clean_name = Path::SanitizeFileName(name); - std::string clean_path = Path::Canonicalize(Path::RealPath(game_path)); - if (!Path::IsValidFileName(clean_name)) - { - QMessageBox::critical(this, tr("Failed to create shortcut"), tr("Filename contains illegal character."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); - return; - } - - // Find the executable path - std::string executable_path = FileSystem::GetPackagePath(); - if (executable_path.empty()) - { - QMessageBox::critical(this, tr("Failed to create shortcut"), tr("Executable path is empty."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); - return; - } - - // Find home directory - std::string link_path; - const char* home = std::getenv("HOME"); - const char* xdg_desktop_dir = std::getenv("XDG_DESKTOP_DIR"); - const char* xdg_data_home = std::getenv("XDG_DATA_HOME"); - if (home) - { - if (is_desktop) - { - if (xdg_desktop_dir) - link_path = fmt::format("{}/{}.desktop", xdg_desktop_dir, clean_name); - else - link_path = fmt::format("{}/Desktop/{}.desktop", home, clean_name); - } - else - { - if (xdg_data_home) - link_path = fmt::format("{}/applications/{}.desktop", xdg_data_home, clean_name); - else - link_path = fmt::format("{}/.local/share/applications/{}.desktop", home, clean_name); - } - } - else - { - QMessageBox::critical(this, tr("Failed to create shortcut"), tr("Path to the Home directory is empty."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); - return; - } - - std::string icon_name; - if (!icon_path.empty()) - { - if (!FileSystem::FileExists(icon_path.c_str())) - { - QMessageBox::critical(this, tr("Failed to create shortcut"), tr("The selected icon file does not exist."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); - return; - } - icon_name = icon_path; - } - else - { - // Copy PCSX2 icon - std::string icon_dest; - if (xdg_data_home) - icon_dest = fmt::format("{}/icons/hicolor/512x512/apps/", xdg_data_home); - else - icon_dest = fmt::format("{}/.local/share/icons/hicolor/512x512/apps/", home); - - if (is_flatpak) // Flatpak - { - executable_path = "flatpak run net.pcsx2.PCSX2"; - icon_name = "net.pcsx2.PCSX2"; - } - else - { - icon_name = "PCSX2"; - std::string icon_path_dest = fmt::format("{}/{}.png", icon_dest, icon_name).c_str(); - if (FileSystem::EnsureDirectoryExists(icon_dest.c_str(), true)) - if (!FileSystem::FileExists(icon_path_dest.c_str())) - FileSystem::CopyFilePath(Path::Combine(EmuFolders::Resources, "icons/AppIconLarge.png").c_str(), icon_path_dest.c_str(), false); - } - } - - // Shortcut CmdLine Args - bool lossless = true; - for (std::string& arg : passed_cli_args) - lossless &= ShortcutCreationDialog::EscapeShortcutCommandLine(&arg); - - if (!lossless) - { - QMessageBox::critical(this, tr("Failed to create shortcut"), tr("File path contains invalid character(s)."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); - return; - } - - std::string cmdline = StringUtil::JoinString(passed_cli_args.begin(), passed_cli_args.end(), " "); - - // Further string sanitization - if (!is_flatpak) - ShortcutCreationDialog::EscapeShortcutCommandLine(&executable_path); - ShortcutCreationDialog::EscapeShortcutCommandLine(&clean_path); - - // Assembling the .desktop file - std::string final_args; - final_args = fmt::format("{} {} {} -- {}", executable_path, cmdline, custom_args, clean_path); - std::string file_content = - "[Desktop Entry]\n" - "Encoding=UTF-8\n" - "Version=1.0\n" - "Type=Application\n" - "Terminal=false\n" - "StartupWMClass=PCSX2\n" - "Exec=" + - final_args + "\n" + - "Name=" + - clean_name + "\n" + - "Icon=" + - icon_name + "\n" + - "Categories=Game;Emulator;\n"; - std::string_view sv(file_content); - - // Prompt user for shortcut saving destination - QString final_path(QStringLiteral("%1").arg(QString::fromStdString(link_path))); - const QString filter(tr("Desktop Shortcut Files (*.desktop)")); - - final_path = QDir::toNativeSeparators(QFileDialog::getSaveFileName(this, tr("Select Shortcut Save Destination"), final_path, filter)); - - if (final_path.isEmpty()) - return; - - // Write to .desktop file - if (!FileSystem::WriteStringToFile(final_path.toStdString().c_str(), sv)) - { - QMessageBox::critical(this, tr("Failed to create shortcut"), tr("Failed to create .desktop file"), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); - return; - } - - if (chmod(final_path.toStdString().c_str(), S_IRWXU) != 0) // enables user to execute file - Console.ErrorFmt("Failed to change file permissions for .desktop file: {} ({})", strerror(errno), errno); -#endif -} - -bool ShortcutCreationDialog::EscapeShortcutCommandLine(std::string* arg) -{ -#ifdef _WIN32 - if (!arg->empty() && arg->find_first_of(" \t\n\v\"") == std::string::npos) - return true; - - std::string temp; - temp.reserve(arg->length() + 10); - temp += '"'; - - for (auto it = arg->begin();; ++it) - { - int backslash_count = 0; - while (it != arg->end() && *it == '\\') - { - ++it; - ++backslash_count; - } - - if (it == arg->end()) - { - temp.append(backslash_count * 2, '\\'); - break; - } - - if (*it == '"') - { - temp.append(backslash_count * 2 + 1, '\\'); - temp += '"'; - } - else - { - temp.append(backslash_count, '\\'); - temp += *it; - } - } - - temp += '"'; - *arg = std::move(temp); - return true; -#else - const char* carg = arg->c_str(); - const char* cend = carg + arg->size(); - const char* RESERVED_CHARS = " \t\n\\\"'\\\\><~|%&;$*?#()`" - "\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0d\x0e\x0f" - "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x7f"; - const char* next = carg + std::strcspn(carg, RESERVED_CHARS); - - if (next == cend) - return true; // No escaping needed, don't modify - - bool lossless = true; - std::string temp = "\""; - const char* NOT_VALID_IN_QUOTE = "%`$\"\\\n" - "\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0d\x0e\x0f" - "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x7f"; - - while (true) - { - next = carg + std::strcspn(carg, NOT_VALID_IN_QUOTE); - temp.append(carg, next); - carg = next; - - if (carg == cend) - break; - - switch (*carg) - { - case '"': - case '`': - temp.push_back('\\'); - temp.push_back(*carg); - break; - case '\\': - temp.append("\\\\\\\\"); - break; - case '$': - temp.push_back('\\'); - temp.push_back('\\'); - temp.push_back(*carg); - break; - case '%': - temp.push_back('%'); - temp.push_back(*carg); - break; - default: - temp.push_back(' '); - lossless = false; - break; - } - ++carg; - } - - temp.push_back('"'); - *arg = std::move(temp); - return lossless; -#endif -} - #include "moc_ShortcutCreationDialog.cpp" diff --git a/pcsx2-qt/ShortcutCreationDialog.h b/pcsx2-qt/ShortcutCreationDialog.h index eee8def535..8f0d22bab3 100644 --- a/pcsx2-qt/ShortcutCreationDialog.h +++ b/pcsx2-qt/ShortcutCreationDialog.h @@ -14,13 +14,6 @@ public: ShortcutCreationDialog(QWidget* parent, const QString& title, const QString& path); ~ShortcutCreationDialog() = default; - /// Create desktop shortcut for games - void CreateShortcut(const std::string name, const std::string game_path, std::vector passed_cli_args, std::string custom_args, const std::string icon_path, bool is_desktop); - - /// Escapes the given string for use with command line arguments. - /// Returns a bool that indicates whether the escaping operation are lossless or not. - bool EscapeShortcutCommandLine(std::string* cmdline); - protected: const QString m_title; const QString m_path;