mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
Add support for opening links in incognito mode on Linux & BSD (#4745)
Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
parent
168f346c81
commit
69c983e0d9
|
@ -23,6 +23,8 @@
|
||||||
- Minor: All channels opened in browser tabs are synced when using the extension for quicker switching between tabs. (#4741)
|
- Minor: All channels opened in browser tabs are synced when using the extension for quicker switching between tabs. (#4741)
|
||||||
- Minor: Show channel point redemptions without messages in usercard. (#4557)
|
- Minor: Show channel point redemptions without messages in usercard. (#4557)
|
||||||
- Minor: Allow for customizing the behavior of `Right Click`ing of usernames. (#4622, #4751)
|
- Minor: Allow for customizing the behavior of `Right Click`ing of usernames. (#4622, #4751)
|
||||||
|
- Minor: Added support for opening incognito links in firefox-esr and chromium. (#4745)
|
||||||
|
- Minor: Added support for opening incognito links under Linux/BSD using XDG. (#4745)
|
||||||
- Bugfix: Increased amount of blocked users loaded from 100 to 1,000. (#4721)
|
- Bugfix: Increased amount of blocked users loaded from 100 to 1,000. (#4721)
|
||||||
- Bugfix: Fixed generation of crashdumps by the browser-extension process when the browser was closed. (#4667)
|
- Bugfix: Fixed generation of crashdumps by the browser-extension process when the browser was closed. (#4667)
|
||||||
- Bugfix: Fix spacing issue with mentions inside RTL text. (#4677)
|
- Bugfix: Fix spacing issue with mentions inside RTL text. (#4677)
|
||||||
|
@ -55,6 +57,7 @@
|
||||||
- Dev: Added the ability to use an alternate linker using the `-DUSE_ALTERNATE_LINKER=...` CMake parameter. (#4711)
|
- Dev: Added the ability to use an alternate linker using the `-DUSE_ALTERNATE_LINKER=...` CMake parameter. (#4711)
|
||||||
- Dev: The Windows installer is now built in CI. (#4408)
|
- Dev: The Windows installer is now built in CI. (#4408)
|
||||||
- Dev: Removed `getApp` and `getSettings` calls from message rendering. (#4535)
|
- Dev: Removed `getApp` and `getSettings` calls from message rendering. (#4535)
|
||||||
|
- Dev: Get the default browser executable instead of the entire command line when opening incognito links. (#4745)
|
||||||
|
|
||||||
## 2.4.4
|
## 2.4.4
|
||||||
|
|
||||||
|
|
|
@ -438,6 +438,12 @@ set(SOURCE_FILES
|
||||||
util/TypeName.hpp
|
util/TypeName.hpp
|
||||||
util/WindowsHelper.cpp
|
util/WindowsHelper.cpp
|
||||||
util/WindowsHelper.hpp
|
util/WindowsHelper.hpp
|
||||||
|
util/XDGDesktopFile.cpp
|
||||||
|
util/XDGDesktopFile.hpp
|
||||||
|
util/XDGDirectory.cpp
|
||||||
|
util/XDGDirectory.hpp
|
||||||
|
util/XDGHelper.cpp
|
||||||
|
util/XDGHelper.hpp
|
||||||
|
|
||||||
util/serialize/Container.hpp
|
util/serialize/Container.hpp
|
||||||
|
|
||||||
|
|
|
@ -55,3 +55,4 @@ Q_LOGGING_CATEGORY(chatterinoWebsocket, "chatterino.websocket", logThreshold);
|
||||||
Q_LOGGING_CATEGORY(chatterinoWidget, "chatterino.widget", logThreshold);
|
Q_LOGGING_CATEGORY(chatterinoWidget, "chatterino.widget", logThreshold);
|
||||||
Q_LOGGING_CATEGORY(chatterinoWindowmanager, "chatterino.windowmanager",
|
Q_LOGGING_CATEGORY(chatterinoWindowmanager, "chatterino.windowmanager",
|
||||||
logThreshold);
|
logThreshold);
|
||||||
|
Q_LOGGING_CATEGORY(chatterinoXDG, "chatterino.xdg", logThreshold);
|
||||||
|
|
|
@ -42,3 +42,4 @@ Q_DECLARE_LOGGING_CATEGORY(chatterinoUpdate);
|
||||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoWebsocket);
|
Q_DECLARE_LOGGING_CATEGORY(chatterinoWebsocket);
|
||||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoWidget);
|
Q_DECLARE_LOGGING_CATEGORY(chatterinoWidget);
|
||||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoWindowmanager);
|
Q_DECLARE_LOGGING_CATEGORY(chatterinoWindowmanager);
|
||||||
|
Q_DECLARE_LOGGING_CATEGORY(chatterinoXDG);
|
||||||
|
|
|
@ -1,88 +1,93 @@
|
||||||
#include "util/IncognitoBrowser.hpp"
|
#include "util/IncognitoBrowser.hpp"
|
||||||
#ifdef USEWINSDK
|
#ifdef USEWINSDK
|
||||||
# include "util/WindowsHelper.hpp"
|
# include "util/WindowsHelper.hpp"
|
||||||
|
#elif defined(Q_OS_UNIX) and !defined(Q_OS_DARWIN)
|
||||||
|
# include "util/XDGHelper.hpp"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include <QProcess>
|
#include <QProcess>
|
||||||
#include <QRegularExpression>
|
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
using namespace chatterino;
|
using namespace chatterino;
|
||||||
|
|
||||||
#ifdef USEWINSDK
|
QString getPrivateSwitch(const QString &browserExecutable)
|
||||||
QString injectPrivateSwitch(QString command)
|
|
||||||
{
|
{
|
||||||
// list of command line switches to turn on private browsing in browsers
|
// list of command line switches to turn on private browsing in browsers
|
||||||
static auto switches = std::vector<std::pair<QString, QString>>{
|
static auto switches = std::vector<std::pair<QString, QString>>{
|
||||||
{"firefox", "-private-window"}, {"librewolf", "-private-window"},
|
{"firefox", "-private-window"}, {"librewolf", "-private-window"},
|
||||||
{"waterfox", "-private-window"}, {"icecat", "-private-window"},
|
{"waterfox", "-private-window"}, {"icecat", "-private-window"},
|
||||||
{"chrome", "-incognito"}, {"vivaldi", "-incognito"},
|
{"chrome", "-incognito"}, {"vivaldi", "-incognito"},
|
||||||
{"opera", "-newprivatetab"}, {"opera\\\\launcher", "--private"},
|
{"opera", "-newprivatetab"}, {"opera\\launcher", "--private"},
|
||||||
{"iexplore", "-private"}, {"msedge", "-inprivate"},
|
{"iexplore", "-private"}, {"msedge", "-inprivate"},
|
||||||
|
{"firefox-esr", "-private-window"}, {"chromium", "-incognito"},
|
||||||
};
|
};
|
||||||
|
|
||||||
// transform into regex and replacement string
|
// compare case-insensitively
|
||||||
std::vector<std::pair<QRegularExpression, QString>> replacers;
|
auto lowercasedBrowserExecutable = browserExecutable.toLower();
|
||||||
|
|
||||||
|
#ifdef Q_OS_WINDOWS
|
||||||
|
if (lowercasedBrowserExecutable.endsWith(".exe"))
|
||||||
|
{
|
||||||
|
lowercasedBrowserExecutable.chop(4);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
for (const auto &switch_ : switches)
|
for (const auto &switch_ : switches)
|
||||||
{
|
{
|
||||||
replacers.emplace_back(
|
if (lowercasedBrowserExecutable.endsWith(switch_.first))
|
||||||
QRegularExpression("(" + switch_.first + "\\.exe\"?).*",
|
|
||||||
QRegularExpression::CaseInsensitiveOption),
|
|
||||||
"\\1 " + switch_.second);
|
|
||||||
}
|
|
||||||
|
|
||||||
// try to find matching regex and apply it
|
|
||||||
for (const auto &replacement : replacers)
|
|
||||||
{
|
|
||||||
if (replacement.first.match(command).hasMatch())
|
|
||||||
{
|
{
|
||||||
command.replace(replacement.first, replacement.second);
|
return switch_.second;
|
||||||
return command;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// couldn't match any browser -> unknown browser
|
// couldn't match any browser -> unknown browser
|
||||||
return QString();
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
QString getCommand()
|
QString getDefaultBrowserExecutable()
|
||||||
{
|
{
|
||||||
|
#ifdef USEWINSDK
|
||||||
// get default browser start command, by protocol if possible, falling back to extension if not
|
// get default browser start command, by protocol if possible, falling back to extension if not
|
||||||
QString command =
|
QString command =
|
||||||
getAssociatedCommand(AssociationQueryType::Protocol, L"http");
|
getAssociatedExecutable(AssociationQueryType::Protocol, L"http");
|
||||||
|
|
||||||
if (command.isNull())
|
if (command.isNull())
|
||||||
{
|
{
|
||||||
// failed to fetch default browser by protocol, try by file extension instead
|
// failed to fetch default browser by protocol, try by file extension instead
|
||||||
command =
|
command = getAssociatedExecutable(AssociationQueryType::FileExtension,
|
||||||
getAssociatedCommand(AssociationQueryType::FileExtension, L".html");
|
L".html");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (command.isNull())
|
if (command.isNull())
|
||||||
{
|
{
|
||||||
// also try the equivalent .htm extension
|
// also try the equivalent .htm extension
|
||||||
command =
|
command = getAssociatedExecutable(AssociationQueryType::FileExtension,
|
||||||
getAssociatedCommand(AssociationQueryType::FileExtension, L".htm");
|
L".htm");
|
||||||
}
|
|
||||||
|
|
||||||
if (command.isNull())
|
|
||||||
{
|
|
||||||
// failed to find browser command
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// inject switch to enable private browsing
|
|
||||||
command = injectPrivateSwitch(command);
|
|
||||||
if (command.isNull())
|
|
||||||
{
|
|
||||||
return QString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return command;
|
return command;
|
||||||
}
|
#elif defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN)
|
||||||
|
static QString defaultBrowser = []() -> QString {
|
||||||
|
auto desktopFile = getDefaultBrowserDesktopFile();
|
||||||
|
if (desktopFile.has_value())
|
||||||
|
{
|
||||||
|
auto entry = desktopFile->getEntries("Desktop Entry");
|
||||||
|
auto exec = entry.find("Exec");
|
||||||
|
if (exec != entry.end())
|
||||||
|
{
|
||||||
|
return parseDesktopExecProgram(exec->second.trimmed());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}();
|
||||||
|
|
||||||
|
return defaultBrowser;
|
||||||
|
#else
|
||||||
|
return {};
|
||||||
#endif
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
@ -90,23 +95,15 @@ namespace chatterino {
|
||||||
|
|
||||||
bool supportsIncognitoLinks()
|
bool supportsIncognitoLinks()
|
||||||
{
|
{
|
||||||
#ifdef USEWINSDK
|
auto browserExe = getDefaultBrowserExecutable();
|
||||||
return !getCommand().isNull();
|
return !browserExe.isNull() && !getPrivateSwitch(browserExe).isNull();
|
||||||
#else
|
|
||||||
return false;
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool openLinkIncognito(const QString &link)
|
bool openLinkIncognito(const QString &link)
|
||||||
{
|
{
|
||||||
#ifdef USEWINSDK
|
auto browserExe = getDefaultBrowserExecutable();
|
||||||
auto command = getCommand();
|
return QProcess::startDetached(browserExe,
|
||||||
|
{getPrivateSwitch(browserExe), link});
|
||||||
// TODO: split command into program path and incognito argument
|
|
||||||
return QProcess::startDetached(command, {link});
|
|
||||||
#else
|
|
||||||
return false;
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
|
@ -88,7 +88,7 @@ void setRegisteredForStartup(bool isRegistered)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QString getAssociatedCommand(AssociationQueryType queryType, LPCWSTR query)
|
QString getAssociatedExecutable(AssociationQueryType queryType, LPCWSTR query)
|
||||||
{
|
{
|
||||||
static HINSTANCE shlwapi = LoadLibrary(L"shlwapi");
|
static HINSTANCE shlwapi = LoadLibrary(L"shlwapi");
|
||||||
if (shlwapi == nullptr)
|
if (shlwapi == nullptr)
|
||||||
|
@ -122,7 +122,7 @@ QString getAssociatedCommand(AssociationQueryType queryType, LPCWSTR query)
|
||||||
}
|
}
|
||||||
|
|
||||||
DWORD resultSize = 0;
|
DWORD resultSize = 0;
|
||||||
if (FAILED(assocQueryString(flags, ASSOCSTR_COMMAND, query, nullptr,
|
if (FAILED(assocQueryString(flags, ASSOCSTR_EXECUTABLE, query, nullptr,
|
||||||
nullptr, &resultSize)))
|
nullptr, &resultSize)))
|
||||||
{
|
{
|
||||||
return QString();
|
return QString();
|
||||||
|
@ -137,8 +137,8 @@ QString getAssociatedCommand(AssociationQueryType queryType, LPCWSTR query)
|
||||||
|
|
||||||
QString result;
|
QString result;
|
||||||
auto buf = new wchar_t[resultSize];
|
auto buf = new wchar_t[resultSize];
|
||||||
if (SUCCEEDED(assocQueryString(flags, ASSOCSTR_COMMAND, query, nullptr, buf,
|
if (SUCCEEDED(assocQueryString(flags, ASSOCSTR_EXECUTABLE, query, nullptr,
|
||||||
&resultSize)))
|
buf, &resultSize)))
|
||||||
{
|
{
|
||||||
// QString::fromWCharArray expects the length in characters *not
|
// QString::fromWCharArray expects the length in characters *not
|
||||||
// including* the null terminator, but AssocQueryStringW calculates
|
// including* the null terminator, but AssocQueryStringW calculates
|
||||||
|
|
|
@ -16,7 +16,7 @@ void flushClipboard();
|
||||||
bool isRegisteredForStartup();
|
bool isRegisteredForStartup();
|
||||||
void setRegisteredForStartup(bool isRegistered);
|
void setRegisteredForStartup(bool isRegistered);
|
||||||
|
|
||||||
QString getAssociatedCommand(AssociationQueryType queryType, LPCWSTR query);
|
QString getAssociatedExecutable(AssociationQueryType queryType, LPCWSTR query);
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
||||||
|
|
118
src/util/XDGDesktopFile.cpp
Normal file
118
src/util/XDGDesktopFile.cpp
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
#include "util/XDGDesktopFile.hpp"
|
||||||
|
|
||||||
|
#include "util/XDGDirectory.hpp"
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
#if defined(Q_OS_UNIX) and !defined(Q_OS_DARWIN)
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
XDGDesktopFile::XDGDesktopFile(const QString &filename)
|
||||||
|
{
|
||||||
|
QFile file(filename);
|
||||||
|
if (!file.open(QIODevice::ReadOnly))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->valid = true;
|
||||||
|
|
||||||
|
std::optional<std::reference_wrapper<XDGEntries>> entries;
|
||||||
|
|
||||||
|
while (!file.atEnd())
|
||||||
|
{
|
||||||
|
auto lineBytes = file.readLine().trimmed();
|
||||||
|
|
||||||
|
// Ignore comments & empty lines
|
||||||
|
if (lineBytes.startsWith('#') || lineBytes.size() == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto line = QString::fromUtf8(lineBytes);
|
||||||
|
|
||||||
|
if (line.startsWith('['))
|
||||||
|
{
|
||||||
|
// group header
|
||||||
|
auto end = line.indexOf(']', 1);
|
||||||
|
if (end == -1 || end == 1)
|
||||||
|
{
|
||||||
|
// malformed header - either empty or no closing bracket
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto groupName = line.mid(1, end - 1);
|
||||||
|
|
||||||
|
// it is against spec for the group name to already exist, but the
|
||||||
|
// parsing behavior for that case is not specified. operator[] will
|
||||||
|
// result in duplicate groups being merged, which makes the most
|
||||||
|
// sense for a read-only parser
|
||||||
|
entries = this->groups[groupName];
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// group entry
|
||||||
|
if (!entries.has_value())
|
||||||
|
{
|
||||||
|
// no group header yet, entry before a group header is against spec
|
||||||
|
// and should be ignored
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto delimiter = line.indexOf('=');
|
||||||
|
if (delimiter == -1)
|
||||||
|
{
|
||||||
|
// line is not a group header or a key value pair, ignore it
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto key = QStringView(line).left(delimiter).trimmed().toString();
|
||||||
|
// QStringView.mid() does not do bounds checking before qt 5.15, so
|
||||||
|
// we have to do it ourselves
|
||||||
|
auto valueStart = delimiter + 1;
|
||||||
|
QString value;
|
||||||
|
if (valueStart < line.size())
|
||||||
|
{
|
||||||
|
value = QStringView(line).mid(valueStart).trimmed().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// existing keys are against spec, so we can overwrite them with
|
||||||
|
// wild abandon
|
||||||
|
entries->get().emplace(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
XDGEntries XDGDesktopFile::getEntries(const QString &groupHeader) const
|
||||||
|
{
|
||||||
|
auto group = this->groups.find(groupHeader);
|
||||||
|
if (group != this->groups.end())
|
||||||
|
{
|
||||||
|
return group->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<XDGDesktopFile> XDGDesktopFile::findDesktopFile(
|
||||||
|
const QString &desktopFileID)
|
||||||
|
{
|
||||||
|
for (const auto &dataDir : getXDGDirectories(XDGDirectoryType::Data))
|
||||||
|
{
|
||||||
|
auto fileName =
|
||||||
|
QDir::cleanPath(dataDir + QDir::separator() + "applications" +
|
||||||
|
QDir::separator() + desktopFileID);
|
||||||
|
XDGDesktopFile desktopFile(fileName);
|
||||||
|
if (desktopFile.isValid())
|
||||||
|
{
|
||||||
|
return desktopFile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino
|
||||||
|
|
||||||
|
#endif
|
49
src/util/XDGDesktopFile.hpp
Normal file
49
src/util/XDGDesktopFile.hpp
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "util/QStringHash.hpp"
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#if defined(Q_OS_UNIX) and !defined(Q_OS_DARWIN)
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
// See https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#group-header
|
||||||
|
using XDGEntries = std::unordered_map<QString, QString>;
|
||||||
|
|
||||||
|
class XDGDesktopFile
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// Read the file at `filename` as an XDG desktop file, parsing its groups & their entries
|
||||||
|
//
|
||||||
|
// Use the `isValid` function to check if the file was read properly
|
||||||
|
explicit XDGDesktopFile(const QString &filename);
|
||||||
|
|
||||||
|
/// Returns a map of entries for the given group header
|
||||||
|
XDGEntries getEntries(const QString &groupHeader) const;
|
||||||
|
|
||||||
|
/// isValid returns true if the file exists and is readable
|
||||||
|
bool isValid() const
|
||||||
|
{
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the first desktop file based on the given desktop file ID
|
||||||
|
///
|
||||||
|
/// This will look through all Data XDG directories
|
||||||
|
///
|
||||||
|
/// Can return std::nullopt if no desktop file was found for the given desktop file ID
|
||||||
|
///
|
||||||
|
/// References: https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s02.html#desktop-file-id
|
||||||
|
static std::optional<XDGDesktopFile> findDesktopFile(
|
||||||
|
const QString &desktopFileID);
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool valid{};
|
||||||
|
std::unordered_map<QString, XDGEntries> groups;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino
|
||||||
|
|
||||||
|
#endif
|
77
src/util/XDGDirectory.cpp
Normal file
77
src/util/XDGDirectory.cpp
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
#include "util/XDGDirectory.hpp"
|
||||||
|
|
||||||
|
#include "util/CombinePath.hpp"
|
||||||
|
#include "util/Qt.hpp"
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
#if defined(Q_OS_UNIX) and !defined(Q_OS_DARWIN)
|
||||||
|
|
||||||
|
QStringList getXDGDirectories(XDGDirectoryType directory)
|
||||||
|
{
|
||||||
|
// User XDG directory environment variables with defaults
|
||||||
|
// Defaults fetched from https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html#variables 2023-08-05
|
||||||
|
static std::unordered_map<XDGDirectoryType,
|
||||||
|
std::pair<const char *, QString>>
|
||||||
|
userDirectories = {
|
||||||
|
{
|
||||||
|
XDGDirectoryType::Config,
|
||||||
|
{
|
||||||
|
"XDG_CONFIG_HOME",
|
||||||
|
combinePath(QDir::homePath(), ".config/"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
XDGDirectoryType::Data,
|
||||||
|
{
|
||||||
|
"XDG_DATA_HOME",
|
||||||
|
combinePath(QDir::homePath(), ".local/share/"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Base (or system) XDG directory environment variables with defaults
|
||||||
|
// Defaults fetched from https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html#variables 2023-08-05
|
||||||
|
static std::unordered_map<XDGDirectoryType,
|
||||||
|
std::pair<const char *, QStringList>>
|
||||||
|
baseDirectories = {
|
||||||
|
{
|
||||||
|
XDGDirectoryType::Config,
|
||||||
|
{
|
||||||
|
"XDG_CONFIG_DIRS",
|
||||||
|
{"/etc/xdg"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
XDGDirectoryType::Data,
|
||||||
|
{
|
||||||
|
"XDG_DATA_DIRS",
|
||||||
|
{"/usr/local/share/", "/usr/share/"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
QStringList paths;
|
||||||
|
|
||||||
|
const auto &[userEnvVar, userDefaultValue] = userDirectories.at(directory);
|
||||||
|
auto userEnvPath = qEnvironmentVariable(userEnvVar, userDefaultValue);
|
||||||
|
paths.push_back(userEnvPath);
|
||||||
|
|
||||||
|
const auto &[baseEnvVar, baseDefaultValue] = baseDirectories.at(directory);
|
||||||
|
auto baseEnvPaths =
|
||||||
|
qEnvironmentVariable(baseEnvVar).split(':', Qt::SkipEmptyParts);
|
||||||
|
if (baseEnvPaths.isEmpty())
|
||||||
|
{
|
||||||
|
paths.append(baseDefaultValue);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
paths.append(baseEnvPaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
} // namespace chatterino
|
21
src/util/XDGDirectory.hpp
Normal file
21
src/util/XDGDirectory.hpp
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QStringList>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
#if defined(Q_OS_UNIX) and !defined(Q_OS_DARWIN)
|
||||||
|
|
||||||
|
enum class XDGDirectoryType {
|
||||||
|
Config,
|
||||||
|
Data,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// getXDGDirectories returns a list of directories given a directory type
|
||||||
|
///
|
||||||
|
/// This will attempt to read the relevant environment variable (e.g. XDG_CONFIG_HOME and XDG_CONFIG_DIRS) and merge them, with sane defaults
|
||||||
|
QStringList getXDGDirectories(XDGDirectoryType directory);
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
} // namespace chatterino
|
259
src/util/XDGHelper.cpp
Normal file
259
src/util/XDGHelper.cpp
Normal file
|
@ -0,0 +1,259 @@
|
||||||
|
#include "util/XDGHelper.hpp"
|
||||||
|
|
||||||
|
#include "common/Literals.hpp"
|
||||||
|
#include "common/QLogging.hpp"
|
||||||
|
#include "util/CombinePath.hpp"
|
||||||
|
#include "util/Qt.hpp"
|
||||||
|
#include "util/XDGDesktopFile.hpp"
|
||||||
|
#include "util/XDGDirectory.hpp"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QProcess>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QSettings>
|
||||||
|
#include <QStringLiteral>
|
||||||
|
#include <QTextCodec>
|
||||||
|
#include <QtGlobal>
|
||||||
|
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
|
#if defined(Q_OS_UNIX) and !defined(Q_OS_DARWIN)
|
||||||
|
|
||||||
|
using namespace chatterino::literals;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||||
|
const auto &LOG = chatterinoXDG;
|
||||||
|
|
||||||
|
using namespace chatterino;
|
||||||
|
|
||||||
|
const auto HTTPS_MIMETYPE = u"x-scheme-handler/https"_s;
|
||||||
|
|
||||||
|
/// Read the given mimeapps file and try to find an association for the HTTPS_MIMETYPE
|
||||||
|
///
|
||||||
|
/// If the mimeapps file is invalid (i.e. wasn't read), return nullopt
|
||||||
|
/// If the file is valid, look for the default Desktop File ID handler for the HTTPS_MIMETYPE
|
||||||
|
/// If no default Desktop File ID handler is found, populate `associations`
|
||||||
|
/// and `denyList` with Desktop File IDs from "Added Associations" and "Removed Associations" respectively
|
||||||
|
std::optional<XDGDesktopFile> processMimeAppsList(
|
||||||
|
const QString &mimeappsPath, QStringList &associations,
|
||||||
|
std::unordered_set<QString> &denyList)
|
||||||
|
{
|
||||||
|
XDGDesktopFile mimeappsFile(mimeappsPath);
|
||||||
|
if (!mimeappsFile.isValid())
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the list of Desktop File IDs for the given mimetype under the "Default
|
||||||
|
// Applications" group in the mimeapps.list file
|
||||||
|
auto defaultGroup = mimeappsFile.getEntries("Default Applications");
|
||||||
|
auto defaultApps = defaultGroup.find(HTTPS_MIMETYPE);
|
||||||
|
if (defaultApps != defaultGroup.cend())
|
||||||
|
{
|
||||||
|
// for each desktop ID in the list:
|
||||||
|
auto desktopIds = defaultApps->second.split(';', Qt::SkipEmptyParts);
|
||||||
|
for (const auto &entry : desktopIds)
|
||||||
|
{
|
||||||
|
auto desktopId = entry.trimmed();
|
||||||
|
|
||||||
|
// if a valid desktop file is found, verify that it is associated
|
||||||
|
// with the type. being in the default list gives it an implicit
|
||||||
|
// association, so just check that it's not in the denylist
|
||||||
|
if (!denyList.contains(desktopId))
|
||||||
|
{
|
||||||
|
auto desktopFile = XDGDesktopFile::findDesktopFile(desktopId);
|
||||||
|
// if a valid association is found, we have found the default
|
||||||
|
// application
|
||||||
|
if (desktopFile.has_value())
|
||||||
|
{
|
||||||
|
return desktopFile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// no definitive default application found. process added and removed
|
||||||
|
// associations, then return empty
|
||||||
|
|
||||||
|
// load any removed associations into the denylist
|
||||||
|
auto removedGroup = mimeappsFile.getEntries("Removed Associations");
|
||||||
|
auto removedApps = removedGroup.find(HTTPS_MIMETYPE);
|
||||||
|
if (removedApps != removedGroup.end())
|
||||||
|
{
|
||||||
|
auto desktopIds = removedApps->second.split(';', Qt::SkipEmptyParts);
|
||||||
|
for (const auto &entry : desktopIds)
|
||||||
|
{
|
||||||
|
denyList.insert(entry.trimmed());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// append any created associations to the associations list
|
||||||
|
auto addedGroup = mimeappsFile.getEntries("Added Associations");
|
||||||
|
auto addedApps = addedGroup.find(HTTPS_MIMETYPE);
|
||||||
|
if (addedApps != addedGroup.end())
|
||||||
|
{
|
||||||
|
auto desktopIds = addedApps->second.split(';', Qt::SkipEmptyParts);
|
||||||
|
for (const auto &entry : desktopIds)
|
||||||
|
{
|
||||||
|
associations.push_back(entry.trimmed());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<XDGDesktopFile> searchMimeAppsListsInDirectory(
|
||||||
|
const QString &directory, QStringList &associations,
|
||||||
|
std::unordered_set<QString> &denyList)
|
||||||
|
{
|
||||||
|
static auto desktopNames = qEnvironmentVariable("XDG_CURRENT_DESKTOP")
|
||||||
|
.split(':', Qt::SkipEmptyParts);
|
||||||
|
static const QString desktopFilename = QStringLiteral("%1-mimeapps.list");
|
||||||
|
static const QString nonDesktopFilename = QStringLiteral("mimeapps.list");
|
||||||
|
|
||||||
|
// try desktop specific mimeapps.list files first
|
||||||
|
for (const auto &desktopName : desktopNames)
|
||||||
|
{
|
||||||
|
auto fileName =
|
||||||
|
combinePath(directory, desktopFilename.arg(desktopName));
|
||||||
|
auto defaultApp = processMimeAppsList(fileName, associations, denyList);
|
||||||
|
if (defaultApp.has_value())
|
||||||
|
{
|
||||||
|
return defaultApp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// try the generic mimeapps.list
|
||||||
|
auto fileName = combinePath(directory, nonDesktopFilename);
|
||||||
|
auto defaultApp = processMimeAppsList(fileName, associations, denyList);
|
||||||
|
if (defaultApp.has_value())
|
||||||
|
{
|
||||||
|
return defaultApp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// no definitive default application found
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
/// Try to figure out the most reasonably default web browser to use
|
||||||
|
///
|
||||||
|
/// If the `xdg-settings` program is available, use that
|
||||||
|
/// If not, read through all possible mimapps files in the order specified here: https://specifications.freedesktop.org/mime-apps-spec/mime-apps-spec-1.0.1.html#file
|
||||||
|
/// If no mimeapps file has a default, try to use the Added Associations in those files
|
||||||
|
std::optional<XDGDesktopFile> getDefaultBrowserDesktopFile()
|
||||||
|
{
|
||||||
|
// no xdg-utils, find it manually by searching mimeapps.list files
|
||||||
|
QStringList associations;
|
||||||
|
std::unordered_set<QString> denyList;
|
||||||
|
|
||||||
|
// config dirs first
|
||||||
|
for (const auto &configDir : getXDGDirectories(XDGDirectoryType::Config))
|
||||||
|
{
|
||||||
|
auto defaultApp =
|
||||||
|
searchMimeAppsListsInDirectory(configDir, associations, denyList);
|
||||||
|
if (defaultApp.has_value())
|
||||||
|
{
|
||||||
|
return defaultApp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// data dirs for backwards compatibility
|
||||||
|
for (const auto &dataDir : getXDGDirectories(XDGDirectoryType::Data))
|
||||||
|
{
|
||||||
|
auto appsDir = combinePath(dataDir, "applications");
|
||||||
|
auto defaultApp =
|
||||||
|
searchMimeAppsListsInDirectory(appsDir, associations, denyList);
|
||||||
|
if (defaultApp.has_value())
|
||||||
|
{
|
||||||
|
return defaultApp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// no mimeapps.list has an explicit default, use the most preferred added
|
||||||
|
// association that exists. We could search here for one we support...
|
||||||
|
if (!associations.empty())
|
||||||
|
{
|
||||||
|
for (const auto &desktopId : associations)
|
||||||
|
{
|
||||||
|
auto desktopFile = XDGDesktopFile::findDesktopFile(desktopId);
|
||||||
|
if (desktopFile.has_value())
|
||||||
|
{
|
||||||
|
return desktopFile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// use xdg-settings if installed
|
||||||
|
QProcess xdgSettings;
|
||||||
|
xdgSettings.start("xdg-settings", {"get", "default-web-browser"},
|
||||||
|
QIODevice::ReadOnly);
|
||||||
|
xdgSettings.waitForFinished(1000);
|
||||||
|
if (xdgSettings.exitStatus() == QProcess::ExitStatus::NormalExit &&
|
||||||
|
xdgSettings.error() == QProcess::UnknownError &&
|
||||||
|
xdgSettings.exitCode() == 0)
|
||||||
|
{
|
||||||
|
return XDGDesktopFile::findDesktopFile(
|
||||||
|
xdgSettings.readAllStandardOutput().trimmed());
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QString parseDesktopExecProgram(const QString &execKey)
|
||||||
|
{
|
||||||
|
static const QRegularExpression unescapeReservedCharacters(
|
||||||
|
R"(\\(["`$\\]))");
|
||||||
|
|
||||||
|
QString program = execKey;
|
||||||
|
|
||||||
|
// string values in desktop files escape all backslashes. This is an
|
||||||
|
// independent escaping scheme that must be processed first
|
||||||
|
program.replace(u"\\\\"_s, u"\\"_s);
|
||||||
|
|
||||||
|
if (!program.startsWith('"'))
|
||||||
|
{
|
||||||
|
// not quoted, trim after the first space (if any)
|
||||||
|
auto end = program.indexOf(' ');
|
||||||
|
if (end != -1)
|
||||||
|
{
|
||||||
|
program = program.left(end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// quoted
|
||||||
|
auto endQuote = program.indexOf('"', 1);
|
||||||
|
if (endQuote == -1)
|
||||||
|
{
|
||||||
|
// No end quote found, the returned program might be malformed
|
||||||
|
program = program.mid(1);
|
||||||
|
qCWarning(LOG).noquote().nospace()
|
||||||
|
<< "Malformed desktop entry key " << program << ", originally "
|
||||||
|
<< execKey << ", you might run into issues";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// End quote found
|
||||||
|
program = program.mid(1, endQuote - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// program now contains the first token of the command line.
|
||||||
|
// this is either the program name with an absolute path, or just the program name
|
||||||
|
// denoting it's a relative path. Either will be handled by QProcess cleanly
|
||||||
|
// now, there is a second escaping scheme specific to the
|
||||||
|
// exec key that must be applied.
|
||||||
|
program.replace(unescapeReservedCharacters, "\\1");
|
||||||
|
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino
|
||||||
|
|
||||||
|
#endif
|
22
src/util/XDGHelper.hpp
Normal file
22
src/util/XDGHelper.hpp
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "util/XDGDesktopFile.hpp"
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
#if defined(Q_OS_UNIX) and !defined(Q_OS_DARWIN)
|
||||||
|
|
||||||
|
std::optional<XDGDesktopFile> getDefaultBrowserDesktopFile();
|
||||||
|
|
||||||
|
/// Parses the given `execKey` and returns the resulting program name, ignoring all arguments
|
||||||
|
///
|
||||||
|
/// Parsing is done in accordance to https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s07.html
|
||||||
|
///
|
||||||
|
/// Note: We do *NOT* support field codes
|
||||||
|
QString parseDesktopExecProgram(const QString &execKey);
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
} // namespace chatterino
|
|
@ -4,6 +4,7 @@ option(CHATTERINO_TEST_USE_PUBLIC_HTTPBIN "Use public httpbin for testing networ
|
||||||
|
|
||||||
set(test_SOURCES
|
set(test_SOURCES
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/main.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/main.cpp
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}/resources/test-resources.qrc
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/ChannelChatters.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/ChannelChatters.cpp
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/AccessGuard.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/AccessGuard.cpp
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/NetworkCommon.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/NetworkCommon.cpp
|
||||||
|
@ -31,6 +32,8 @@ set(test_SOURCES
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/LinkParser.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/LinkParser.cpp
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/InputCompletion.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/InputCompletion.cpp
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/Literals.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/Literals.cpp
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}/src/XDGDesktopFile.cpp
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}/src/XDGHelper.cpp
|
||||||
# Add your new file above this line!
|
# Add your new file above this line!
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -62,4 +65,9 @@ if(CHATTERINO_TEST_USE_PUBLIC_HTTPBIN)
|
||||||
target_compile_definitions(${PROJECT_NAME} PRIVATE CHATTERINO_TEST_USE_PUBLIC_HTTPBIN)
|
target_compile_definitions(${PROJECT_NAME} PRIVATE CHATTERINO_TEST_USE_PUBLIC_HTTPBIN)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
set_target_properties(${PROJECT_NAME}
|
||||||
|
PROPERTIES
|
||||||
|
AUTORCC ON
|
||||||
|
)
|
||||||
|
|
||||||
gtest_discover_tests(${PROJECT_NAME})
|
gtest_discover_tests(${PROJECT_NAME})
|
||||||
|
|
32
tests/resources/001-mimeapps.list
Normal file
32
tests/resources/001-mimeapps.list
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
thisshould=beignored
|
||||||
|
# this is a comment
|
||||||
|
|
||||||
|
[test]
|
||||||
|
foo=bar
|
||||||
|
# cool comment 😂
|
||||||
|
zoo=zar
|
||||||
|
|
||||||
|
[Default Applications]
|
||||||
|
lol=
|
||||||
|
x-scheme-handler/http=firefox.desktop
|
||||||
|
x-scheme-handler/https=firefox.desktop
|
||||||
|
x-scheme-handler/chrome=firefox.desktop
|
||||||
|
text/html=firefox.desktop
|
||||||
|
application/x-extension-htm=firefox.desktop
|
||||||
|
application/x-extension-html=firefox.desktop
|
||||||
|
application/x-extension-shtml=firefox.desktop
|
||||||
|
application/xhtml+xml=firefox.desktop
|
||||||
|
application/x-extension-xhtml=firefox.desktop
|
||||||
|
application/x-extension-xht=firefox.desktop
|
||||||
|
|
||||||
|
[Added Associations]
|
||||||
|
x-scheme-handler/http=firefox.desktop;
|
||||||
|
x-scheme-handler/https=firefox.desktop;
|
||||||
|
x-scheme-handler/chrome=firefox.desktop;
|
||||||
|
text/html=firefox.desktop;
|
||||||
|
application/x-extension-htm=firefox.desktop;
|
||||||
|
application/x-extension-html=firefox.desktop;
|
||||||
|
application/x-extension-shtml=firefox.desktop;
|
||||||
|
application/xhtml+xml=firefox.desktop;
|
||||||
|
application/x-extension-xhtml=firefox.desktop;
|
||||||
|
application/x-extension-xht=firefox.desktop;
|
5
tests/resources/test-resources.qrc
Normal file
5
tests/resources/test-resources.qrc
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<RCC>
|
||||||
|
<qresource prefix="/">
|
||||||
|
<file>001-mimeapps.list</file>
|
||||||
|
</qresource>
|
||||||
|
</RCC>
|
19
tests/src/XDGDesktopFile.cpp
Normal file
19
tests/src/XDGDesktopFile.cpp
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
#include "util/XDGDesktopFile.hpp"
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
using namespace chatterino;
|
||||||
|
|
||||||
|
TEST(XDGDesktopFile, String)
|
||||||
|
{
|
||||||
|
auto desktopFile = XDGDesktopFile(":/001-mimeapps.list");
|
||||||
|
auto entries = desktopFile.getEntries("Default Applications");
|
||||||
|
|
||||||
|
ASSERT_EQ(entries["thisshould"], "");
|
||||||
|
|
||||||
|
ASSERT_EQ(entries["lol"], "");
|
||||||
|
ASSERT_EQ(entries["x-scheme-handler/http"], QString("firefox.desktop"));
|
||||||
|
|
||||||
|
ASSERT_EQ(desktopFile.getEntries("test").size(), 2);
|
||||||
|
}
|
62
tests/src/XDGHelper.cpp
Normal file
62
tests/src/XDGHelper.cpp
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
#include "util/XDGHelper.hpp"
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
using namespace chatterino;
|
||||||
|
|
||||||
|
TEST(XDGHelper, ParseDesktopExecProgram)
|
||||||
|
{
|
||||||
|
struct TestCase {
|
||||||
|
QString input;
|
||||||
|
QString expectedOutput;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<TestCase> testCases{
|
||||||
|
{
|
||||||
|
// Sanity check: Ensure simple Exec lines aren't made messed with
|
||||||
|
"firefox",
|
||||||
|
"firefox",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Simple trim after the first space
|
||||||
|
"/usr/lib/firefox/firefox %u",
|
||||||
|
"/usr/lib/firefox/firefox",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Simple unquote
|
||||||
|
"\"/usr/lib/firefox/firefox\"",
|
||||||
|
"/usr/lib/firefox/firefox",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Unquote + trim
|
||||||
|
"\"/usr/lib/firefox/firefox\" %u",
|
||||||
|
"/usr/lib/firefox/firefox",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Test malformed exec key (only one quote)
|
||||||
|
"\"/usr/lib/firefox/firefox",
|
||||||
|
"/usr/lib/firefox/firefox",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Quoted executable name with space
|
||||||
|
"\"/usr/bin/my cool browser\"",
|
||||||
|
"/usr/bin/my cool browser",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Executable name with reserved character
|
||||||
|
"/usr/bin/\\$hadowwizardmoneybrowser",
|
||||||
|
"/usr/bin/$hadowwizardmoneybrowser",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto &test : testCases)
|
||||||
|
{
|
||||||
|
auto output = parseDesktopExecProgram(test.input);
|
||||||
|
|
||||||
|
EXPECT_EQ(output, test.expectedOutput)
|
||||||
|
<< "Input '" << test.input.toStdString() << "' failed. Expected '"
|
||||||
|
<< test.expectedOutput.toStdString() << "' but got '"
|
||||||
|
<< output.toStdString() << "'";
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue