From c692c21b87815084f0900da9812b789364919297 Mon Sep 17 00:00:00 2001 From: Xavier Michelon Date: Wed, 10 Apr 2024 14:30:18 +0200 Subject: [PATCH] fix(BRIDGE-8): more robust command-line args parser in bridge-gui. fix(BRIDGE-8): add command-line invocation to log. --- cmd/launcher/main_test.go | 40 +++--- .../bridge-gui/bridge-gui/CommandLine.cpp | 133 +++++++++--------- .../bridge-gui/bridge-gui/CommandLine.h | 2 +- .../frontend/bridge-gui/bridge-gui/main.cpp | 64 ++++----- .../bridge-gui/bridgepp/Test/TestCLI.cpp | 36 ++++- .../bridgepp/bridgepp/CLI/CLIUtils.cpp | 47 +++++++ .../bridgepp/bridgepp/CLI/CLIUtils.h | 7 +- 7 files changed, 194 insertions(+), 135 deletions(-) diff --git a/cmd/launcher/main_test.go b/cmd/launcher/main_test.go index 762f5c66..9f14a537 100644 --- a/cmd/launcher/main_test.go +++ b/cmd/launcher/main_test.go @@ -21,9 +21,7 @@ import ( "testing" "github.com/ProtonMail/proton-bridge/v3/internal/logging" - "github.com/bradenaw/juniper/xslices" "github.com/stretchr/testify/assert" - "golang.org/x/exp/slices" ) func TestFindAndStrip(t *testing.T) { @@ -31,53 +29,53 @@ func TestFindAndStrip(t *testing.T) { result, found := findAndStrip(list, "a") assert.True(t, found) - assert.True(t, xslices.Equal(result, []string{"b", "c", "c", "b", "c"})) + assert.Equal(t, result, []string{"b", "c", "c", "b", "c"}) result, found = findAndStrip(list, "c") assert.True(t, found) - assert.True(t, xslices.Equal(result, []string{"a", "b", "b"})) + assert.Equal(t, result, []string{"a", "b", "b"}) result, found = findAndStrip([]string{"c", "c", "c"}, "c") assert.True(t, found) - assert.True(t, xslices.Equal(result, []string{})) + assert.Equal(t, result, []string{}) result, found = findAndStrip(list, "A") assert.False(t, found) - assert.True(t, xslices.Equal(result, list)) + assert.Equal(t, result, list) result, found = findAndStrip([]string{}, "a") assert.False(t, found) - assert.True(t, xslices.Equal(result, []string{})) + assert.Equal(t, result, []string{}) } func TestFindAndStripWait(t *testing.T) { result, found, values := findAndStripWait([]string{"a", "b", "c"}) assert.False(t, found) - assert.True(t, xslices.Equal(result, []string{"a", "b", "c"})) - assert.True(t, xslices.Equal(values, []string{})) + assert.Equal(t, result, []string{"a", "b", "c"}) + assert.Equal(t, values, []string{}) result, found, values = findAndStripWait([]string{"a", "--wait", "b"}) assert.True(t, found) - assert.True(t, xslices.Equal(result, []string{"a"})) - assert.True(t, xslices.Equal(values, []string{"b"})) + assert.Equal(t, result, []string{"a"}) + assert.Equal(t, values, []string{"b"}) result, found, values = findAndStripWait([]string{"a", "--wait", "b", "--wait", "c"}) assert.True(t, found) - assert.True(t, xslices.Equal(result, []string{"a"})) - assert.True(t, xslices.Equal(values, []string{"b", "c"})) + assert.Equal(t, result, []string{"a"}) + assert.Equal(t, values, []string{"b", "c"}) result, found, values = findAndStripWait([]string{"a", "--wait", "b", "--wait", "c", "--wait", "d"}) assert.True(t, found) - assert.True(t, xslices.Equal(result, []string{"a"})) - assert.True(t, xslices.Equal(values, []string{"b", "c", "d"})) + assert.Equal(t, result, []string{"a"}) + assert.Equal(t, values, []string{"b", "c", "d"}) } func TestAppendOrModifySessionID(t *testing.T) { sessionID := string(logging.NewSessionID()) - assert.True(t, slices.Equal(appendOrModifySessionID(nil, sessionID), []string{"--session-id", sessionID})) - assert.True(t, slices.Equal(appendOrModifySessionID([]string{}, sessionID), []string{"--session-id", sessionID})) - assert.True(t, slices.Equal(appendOrModifySessionID([]string{"--cli"}, sessionID), []string{"--cli", "--session-id", sessionID})) - assert.True(t, slices.Equal(appendOrModifySessionID([]string{"--cli", "--session-id"}, sessionID), []string{"--cli", "--session-id", sessionID})) - assert.True(t, slices.Equal(appendOrModifySessionID([]string{"--cli", "--session-id"}, sessionID), []string{"--cli", "--session-id", sessionID})) - assert.True(t, slices.Equal(appendOrModifySessionID([]string{"--session-id", "", "--cli"}, sessionID), []string{"--session-id", sessionID, "--cli"})) + assert.Equal(t, appendOrModifySessionID(nil, sessionID), []string{"--session-id", sessionID}) + assert.Equal(t, appendOrModifySessionID([]string{}, sessionID), []string{"--session-id", sessionID}) + assert.Equal(t, appendOrModifySessionID([]string{"--cli"}, sessionID), []string{"--cli", "--session-id", sessionID}) + assert.Equal(t, appendOrModifySessionID([]string{"--cli", "--session-id"}, sessionID), []string{"--cli", "--session-id", sessionID}) + assert.Equal(t, appendOrModifySessionID([]string{"--cli", "--session-id"}, sessionID), []string{"--cli", "--session-id", sessionID}) + assert.Equal(t, appendOrModifySessionID([]string{"--session-id", "", "--cli"}, sessionID), []string{"--session-id", sessionID, "--cli"}) } diff --git a/internal/frontend/bridge-gui/bridge-gui/CommandLine.cpp b/internal/frontend/bridge-gui/bridge-gui/CommandLine.cpp index 040be37f..51f9d649 100644 --- a/internal/frontend/bridge-gui/bridge-gui/CommandLine.cpp +++ b/internal/frontend/bridge-gui/bridge-gui/CommandLine.cpp @@ -15,113 +15,104 @@ // You should have received a copy of the GNU General Public License // along with Proton Mail Bridge. If not, see . - #include "Pch.h" #include "CommandLine.h" #include "Settings.h" +#include #include - using namespace bridgepp; - namespace { - -QString const launcherFlag = "--launcher"; ///< launcher flag parameter used for bridge. -QString const noWindowFlag = "--no-window"; ///< The no-window command-line flag. -QString const softwareRendererFlag = "--software-renderer"; ///< The 'software-renderer' command-line flag. enable software rendering for a single execution -QString const setSoftwareRendererFlag = "--set-software-renderer"; ///< The 'set-software-renderer' command-line flag. Software rendering will be used for all subsequent executions of the application. -QString const setHardwareRendererFlag = "--set-hardware-renderer"; ///< The 'set-hardware-renderer' command-line flag. Hardware rendering will be used for all subsequent executions of the application. - - -//**************************************************************************************************************************************************** -/// \brief parse a command-line string argument as expected by go's CLI package. -/// \param[in] argc The number of arguments passed to the application. -/// \param[in] argv The list of arguments passed to the application. -/// \param[in] paramNames the list of names for the parameter -//**************************************************************************************************************************************************** -QString parseGoCLIStringArgument(int argc, char *argv[], QStringList paramNames) { - // go cli package is pretty permissive when it comes to parsing arguments. For each name 'param', all the following seems to be accepted: - // -param value - // --param value - // -param=value - // --param=value - for (QString const ¶mName: paramNames) { - for (qsizetype i = 1; i < argc; ++i) { - QString const arg(QString::fromLocal8Bit(argv[i])); - if ((i < argc - 1) && ((arg == "-" + paramName) || (arg == "--" + paramName))) { - return QString(argv[i + 1]); - } - - QRegularExpressionMatch match = QRegularExpression(QString("^-{1,2}%1=(.+)$").arg(paramName)).match(arg); - if (match.hasMatch()) { - return match.captured(1); - } - } - } - - return QString(); -} - +QString const hyphenatedLauncherFlag = "--launcher"; ///< launcher flag parameter used for bridge. +QString const hyphenatedWindowFlag = "--no-window"; ///< The no-window command-line flag. +QString const hyphenatedSoftwareRendererFlag = "--software-renderer"; ///< The 'software-renderer' command-line flag. enable software rendering for a single execution +QString const hyphenatedSetSoftwareRendererFlag = "--set-software-renderer"; ///< The 'set-software-renderer' command-line flag. Software rendering will be used for all subsequent executions of the application. +QString const hyphenatedSetHardwareRendererFlag = "--set-hardware-renderer"; ///< The 'set-hardware-renderer' command-line flag. Hardware rendering will be used for all subsequent executions of the application. +QString const sessionIDFlag = "session-id"; +QString const hyphenatedSessionIDFlag = "--" + sessionIDFlag; //**************************************************************************************************************************************************** /// \brief Parse the log level from the command-line arguments. /// -/// \param[in] argc The number of arguments passed to the application. -/// \param[in] argv The list of arguments passed to the application. +/// \param[in] args The command-line arguments. /// \return The log level. if not specified on the command-line, the default log level is returned. //**************************************************************************************************************************************************** -Log::Level parseLogLevel(int argc, char *argv[]) { - QString levelStr = parseGoCLIStringArgument(argc, argv, { "l", "log-level" }); +Log::Level parseLogLevel(QStringList const &args) { + QStringList levelStr = parseGoCLIStringArgument(args, {"l", "log-level"}); if (levelStr.isEmpty()) { return Log::defaultLevel; } Log::Level level = Log::defaultLevel; - Log::stringToLevel(levelStr, level); + Log::stringToLevel(levelStr.back(), level); return level; } +//**************************************************************************************************************************************************** +/// \brief Return the most recent sessionID parsed in command-line arguments +/// +/// \param[in] args The command-line arguments. +/// \return The most recent sessionID in the list. If the list is empty, a new sessionID is created. +//**************************************************************************************************************************************************** +QString mostRecentSessionID(QStringList const& args) { + QStringList const sessionIDs = parseGoCLIStringArgument(args, {sessionIDFlag}); + if (sessionIDs.isEmpty()) { + return newSessionID(); + } + + return std::ranges::max(sessionIDs, [](QString const &lhs, QString const &rhs) -> bool { + return sessionIDToDateTime(lhs) < sessionIDToDateTime(rhs); + }); +} } // anonymous namespace - //**************************************************************************************************************************************************** -/// \param[in] argc number of arguments passed to the application. -/// \param[in] argv list of arguments passed to the application. +/// \param[in] argv list of arguments passed to the application, including the exe name/path at index 0. /// \return The parsed options. //**************************************************************************************************************************************************** -CommandLineOptions parseCommandLine(int argc, char *argv[]) { +CommandLineOptions parseCommandLine(QStringList const &argv) { CommandLineOptions options; - bool flagFound = false; - options.launcher = QString::fromLocal8Bit(argv[0]); + bool launcherFlagFound = false; + options.launcher = argv[0]; // for unknown reasons, on Windows QCoreApplication::arguments() frequently returns an empty list, which is incorrect, so we rebuild the argument // list from the original argc and argv values. - for (int i = 1; i < argc; i++) { - QString const &arg = QString::fromLocal8Bit(argv[i]); + for (int i = 1; i < argv.count(); i++) { + QString const &arg = argv[i]; // we can't use QCommandLineParser here since it will fail on unknown options. + + // we skip session-id for now we'll process it later, with a special treatment for duplicates + if (arg == hyphenatedSessionIDFlag) { + i++; // we skip the next param, which if the flag's value. + continue; + } + if (arg.startsWith(hyphenatedSessionIDFlag + "=")) { + continue; + } + // Arguments may contain some bridge flags. - if (arg == softwareRendererFlag) { + if (arg == hyphenatedSoftwareRendererFlag) { options.bridgeGuiArgs.append(arg); options.useSoftwareRenderer = true; } - if (arg == setSoftwareRendererFlag) { + if (arg == hyphenatedSetSoftwareRendererFlag) { app().settings().setUseSoftwareRenderer(true); continue; // setting is permanent. no need to keep/pass it to bridge for restart. } - if (arg == setHardwareRendererFlag) { + if (arg == hyphenatedSetHardwareRendererFlag) { app().settings().setUseSoftwareRenderer(false); continue; // setting is permanent. no need to keep/pass it to bridge for restart. } - if (arg == noWindowFlag) { + if (arg == hyphenatedWindowFlag) { options.noWindow = true; } - if (arg == launcherFlag) { + if (arg == hyphenatedLauncherFlag) { options.bridgeArgs.append(arg); - options.launcher = QString::fromLocal8Bit(argv[++i]); + options.launcher = argv[++i]; options.bridgeArgs.append(options.launcher); - flagFound = true; + launcherFlagFound = true; } #ifdef QT_DEBUG else if (arg == "--attach" || arg == "-a") { @@ -135,22 +126,24 @@ CommandLineOptions parseCommandLine(int argc, char *argv[]) { options.bridgeGuiArgs.append(arg); } } - if (!flagFound) { + if (!launcherFlagFound) { // add bridge-gui as launcher - options.bridgeArgs.append(launcherFlag); + options.bridgeArgs.append(hyphenatedLauncherFlag); options.bridgeArgs.append(options.launcher); } - options.logLevel = parseLogLevel(argc, argv); - - QString sessionID = parseGoCLIStringArgument(argc, argv, { "session-id" }); - if (sessionID.isEmpty()) { - // The session ID was not passed to us on the command-line -> create one and add to the command-line for bridge - sessionID = newSessionID(); - options.bridgeArgs.append("--session-id"); - options.bridgeArgs.append(sessionID); + QStringList args; + if (!argv.isEmpty()) { + args = argv.last(argv.count() - 1); } + + options.logLevel = parseLogLevel(args); + + QString const sessionID = mostRecentSessionID(args); + options.bridgeArgs.append(hyphenatedSessionIDFlag); + options.bridgeArgs.append(sessionID); app().setSessionID(sessionID); return options; } + diff --git a/internal/frontend/bridge-gui/bridge-gui/CommandLine.h b/internal/frontend/bridge-gui/bridge-gui/CommandLine.h index 4db97abc..0b79016f 100644 --- a/internal/frontend/bridge-gui/bridge-gui/CommandLine.h +++ b/internal/frontend/bridge-gui/bridge-gui/CommandLine.h @@ -37,7 +37,7 @@ struct CommandLineOptions { }; -CommandLineOptions parseCommandLine(int argc, char *argv[]); ///< Parse the command-line arguments +CommandLineOptions parseCommandLine(QStringList const &argv); ///< Parse the command-line arguments #endif //BRIDGE_GUI_COMMAND_LINE_H diff --git a/internal/frontend/bridge-gui/bridge-gui/main.cpp b/internal/frontend/bridge-gui/bridge-gui/main.cpp index f1789478..5b364532 100644 --- a/internal/frontend/bridge-gui/bridge-gui/main.cpp +++ b/internal/frontend/bridge-gui/bridge-gui/main.cpp @@ -15,7 +15,6 @@ // You should have received a copy of the GNU General Public License // along with Proton Mail Bridge. If not, see . - #include "BridgeApp.h" #include "BuildConfig.h" #include "CommandLine.h" @@ -30,13 +29,12 @@ #include #include +#include "bridgepp/CLI/CLIUtils.h" #ifdef Q_OS_MACOS - #include "MacOS/SecondInstance.h" - #endif using namespace bridgepp; @@ -50,17 +48,14 @@ QString const exeSuffix = ".exe"; QString const exeSuffix; #endif - QString const bridgeLock = "bridge-v3.lock"; ///< The file name used for the bridge-gui lock file. QString const bridgeGUILock = "bridge-v3-gui.lock"; ///< The file name used for the bridge-gui lock file. QString const exeName = "bridge" + exeSuffix; ///< The bridge executable file name.* -qint64 const grpcServiceConfigWaitDelayMs = 180000; ///< The wait delay for the gRPC config file in milliseconds. +qint64 constexpr grpcServiceConfigWaitDelayMs = 180000; ///< The wait delay for the gRPC config file in milliseconds. QString const waitFlag = "--wait"; ///< The wait command-line flag. - } // anonymous namespace - //**************************************************************************************************************************************************** /// \return The path of the bridge executable. /// \return A null string if the executable could not be located. @@ -70,7 +65,6 @@ QString locateBridgeExe() { return (fileInfo.exists() && fileInfo.isFile() && fileInfo.isExecutable()) ? fileInfo.absoluteFilePath() : QString(); } - //**************************************************************************************************************************************************** /// // initialize the Qt application. //**************************************************************************************************************************************************** @@ -97,8 +91,6 @@ void initQtApplication() { #endif // #ifdef Q_OS_MACOS } - - //**************************************************************************************************************************************************** /// \param[in] engine The QML component. //**************************************************************************************************************************************************** @@ -118,13 +110,12 @@ QQmlComponent *createRootQmlComponent(QQmlApplicationEngine &engine) { rootComponent->loadUrl(QUrl(qrcQmlDir + "/Bridge.qml")); if (rootComponent->status() != QQmlComponent::Status::Ready) { QString const &err = rootComponent->errorString(); - app().log().error(err); + app().log().error(err); throw Exception("Could not load QML component", err); } return rootComponent; } - //**************************************************************************************************************************************************** /// \param[in] lock The lock file to be checked. /// \return True if the lock can be taken, false otherwise. @@ -155,7 +146,6 @@ bool checkSingleInstance(QLockFile &lock) { return true; } - //**************************************************************************************************************************************************** /// \return QUrl to reach the bridge API. //**************************************************************************************************************************************************** @@ -184,7 +174,6 @@ QUrl getApiUrl() { return url; } - //**************************************************************************************************************************************************** /// \brief Check if bridge is running. /// @@ -199,7 +188,6 @@ bool isBridgeRunning() { return (!lockFile.tryLock()) && (lockFile.error() == QLockFile::LockFailedError); } - //**************************************************************************************************************************************************** /// \brief Use api to bring focus on existing bridge instance. //**************************************************************************************************************************************************** @@ -213,8 +201,7 @@ void focusOtherInstance() { if (!sc.load(path)) { throw Exception("The gRPC focus service configuration file is invalid."); } - } - else { + } else { throw Exception("Server did not provide gRPC Focus service configuration."); } @@ -225,20 +212,18 @@ void focusOtherInstance() { if (!client.raise("focusOtherInstance").ok()) { throw Exception(QString("The raise call to the bridge focus service failed.")); } - } - catch (Exception const &e) { + } catch (Exception const &e) { app().log().error(e.qwhat()); auto uuid = reportSentryException("Exception occurred during focusOtherInstance()", e); app().log().fatal(QString("reportID: %1 Captured exception: %2").arg(QByteArray(uuid.bytes, 16).toHex(), e.qwhat())); } } - //**************************************************************************************************************************************************** /// \param [in] args list of arguments to pass to bridge. /// \return bridge executable path //**************************************************************************************************************************************************** -const QString launchBridge(QStringList const &args) { +QString launchBridge(QStringList const &args) { UPOverseer &overseer = app().bridgeOverseer(); overseer.reset(); @@ -251,26 +236,38 @@ const QString launchBridge(QStringList const &args) { } qint64 const pid = qApp->applicationPid(); - QStringList const params = QStringList { "--grpc", "--parent-pid", QString::number(pid) } + args; + QStringList const params = QStringList{"--grpc", "--parent-pid", QString::number(pid)} + args; app().log().info(QString("Launching bridge process with command \"%1\" %2").arg(bridgeExePath, params.join(" "))); overseer = std::make_unique(new ProcessMonitor(bridgeExePath, params, nullptr), nullptr); overseer->startWorker(true); return bridgeExePath; } - //**************************************************************************************************************************************************** // //**************************************************************************************************************************************************** void closeBridgeApp() { app().grpc().quit(); // this will cause the grpc service and the bridge app to close. - UPOverseer &overseer = app().bridgeOverseer(); - if (overseer) { // A null overseer means the app was run in 'attach' mode. We're not monitoring it. + UPOverseer const &overseer = app().bridgeOverseer(); + if (overseer) { + // A null overseer means the app was run in 'attach' mode. We're not monitoring it. + // ReSharper disable once CppExpressionWithoutSideEffects overseer->wait(Overseer::maxTerminationWaitTimeMs); } } +//**************************************************************************************************************************************************** +/// \param[in] argv The command-line argments, including the application name at index 0. +//**************************************************************************************************************************************************** +void logCommandLineInvocation(QStringList argv) { + Log &log = app().log(); + if (argv.isEmpty()) { + log.error("The command line is empty"); + } + log.info("bridge-gui executable: " + argv.front()); + log.info("Command-line invocation: " + (argv.size() > 1 ? argv.last(argv.size() - 1).join(" ") : "")); +} //**************************************************************************************************************************************************** /// \param[in] argc The number of command-line arguments. @@ -289,12 +286,11 @@ int main(int argc, char *argv[]) { auto sentryCloser = qScopeGuard([] { sentry_close(); }); try { - QString const& configDir = bridgepp::userConfigDir(); - + QString const &configDir = bridgepp::userConfigDir(); initQtApplication(); - - CommandLineOptions const cliOptions = parseCommandLine(argc, argv); + QStringList const argvList = cliArgsToStringList(argc, argv); + CommandLineOptions const cliOptions = parseCommandLine(argvList); Log &log = initLog(); log.setLevel(cliOptions.logLevel); @@ -309,6 +305,8 @@ int main(int argc, char *argv[]) { setDockIconVisibleState(!cliOptions.noWindow); #endif + logCommandLineInvocation(argvList); + // In attached mode, we do not intercept stderr and stdout of bridge, as we did not launch it ourselves, so we output the log to the console. // When not in attached mode, log entries are forwarded to bridge, which output it on stdout/stderr. bridge-gui's process monitor intercept // these outputs and output them on the command-line. @@ -348,7 +346,6 @@ int main(int argc, char *argv[]) { QQuickWindow::setSceneGraphBackend((app().settings().useSoftwareRenderer() || cliOptions.useSoftwareRenderer) ? "software" : "rhi"); log.info(QString("Qt Quick renderer: %1").arg(QQuickWindow::sceneGraphBackend())); - QQmlApplicationEngine engine; std::unique_ptr rootComponent(createRootQmlComponent(engine)); std::unique_ptr rootObject(rootComponent->create(engine.rootContext())); @@ -374,7 +371,7 @@ int main(int argc, char *argv[]) { app().log().debug(QString("Monitoring Bridge PID : %1").arg(status.pid)); connection = QObject::connect(bridgeMonitor, &ProcessMonitor::processExited, [&](int returnCode) { - bridgeExited = true;// clazy:exclude=lambda-in-connect + bridgeExited = true; // clazy:exclude=lambda-in-connect qGuiApp->exit(returnCode); }); } @@ -383,7 +380,7 @@ int main(int argc, char *argv[]) { int result = 0; if (!startError) { // we succeeded in launching bridge, so we can be set as mainExecutable. - QString mainexec = QString::fromLocal8Bit(argv[0]); + QString const mainexec = argvList[0]; app().grpc().setMainExecutable(mainexec); QStringList args = cliOptions.bridgeGuiArgs; args.append(waitFlag); @@ -412,8 +409,7 @@ int main(int argc, char *argv[]) { // release the lock file lock.unlock(); return result; - } - catch (Exception const &e) { + } catch (Exception const &e) { sentry_uuid_s const uuid = reportSentryException("Exception occurred during main", e); QString message = e.qwhat(); if (e.showSupportLink()) { diff --git a/internal/frontend/bridge-gui/bridgepp/Test/TestCLI.cpp b/internal/frontend/bridge-gui/bridgepp/Test/TestCLI.cpp index 9c5266dc..d3f064a8 100644 --- a/internal/frontend/bridge-gui/bridgepp/Test/TestCLI.cpp +++ b/internal/frontend/bridge-gui/bridgepp/Test/TestCLI.cpp @@ -23,16 +23,15 @@ using namespace bridgepp; - //**************************************************************************************************************************************************** // //**************************************************************************************************************************************************** TEST(CLI, stripStringParameterFromCommandLine) { - struct Test { + struct TestData { QStringList input; QStringList expectedOutput; }; - QList const tests = { + QList const tests = { {{}, {}}, {{ "--a", "-b", "--C" }, { "--a", "-b", "--C" } }, {{ "--string", "value" }, {} }, @@ -44,7 +43,36 @@ TEST(CLI, stripStringParameterFromCommandLine) { {{ "--string", "--string", "value", "-b", "--string"}, { "value", "-b" } }, }; - for (Test const& test: tests) { + for (TestData const& test: tests) { EXPECT_EQ(stripStringParameterFromCommandLine("--string", test.input), test.expectedOutput); } } + + +TEST(CLI, parseGoCLIStringArgument) { + struct TestData { + QStringList args; + QStringList params; + QStringList expectedOutput; + }; + + QList const tests = { + { {}, {}, {} }, + { {"-param"}, {"param"}, {} }, + { {"--param", "1"}, {"param"}, { "1" } }, + { {"--param", "1","p", "-p", "2", "-flag", "-param=3", "--p=4"}, {"param", "p"}, { "1", "2", "3", "4" } }, + { {"--param", "--param", "1"}, {"param"}, { "--param" } }, + }; + + for (TestData const& test: tests) { + EXPECT_EQ(parseGoCLIStringArgument(test.args, test.params), test.expectedOutput); + } +} + +TEST(CLI, cliArgsToStringList) { + int constexpr argc = 3; + char *argv[] = { const_cast("1"), const_cast("2"), const_cast("3") }; + QStringList const strList { "1", "2", "3" }; + EXPECT_EQ(cliArgsToStringList(argc,argv), strList); + EXPECT_EQ(cliArgsToStringList(0, nullptr), QStringList {}); +} diff --git a/internal/frontend/bridge-gui/bridgepp/bridgepp/CLI/CLIUtils.cpp b/internal/frontend/bridge-gui/bridgepp/bridgepp/CLI/CLIUtils.cpp index 9c6a9daf..31b53ff2 100644 --- a/internal/frontend/bridge-gui/bridgepp/bridgepp/CLI/CLIUtils.cpp +++ b/internal/frontend/bridge-gui/bridgepp/bridgepp/CLI/CLIUtils.cpp @@ -42,4 +42,51 @@ QStringList stripStringParameterFromCommandLine(QString const ¶mName, QStrin } +//**************************************************************************************************************************************************** +/// The flags may be present more than once in the args. All values are returned in order of appearance. +/// +/// \param[in] args The arguments +/// \param[in] paramNames the list of names for the parameter, without any prefix hypen. +/// \return The values found for the flag. +//**************************************************************************************************************************************************** +QStringList parseGoCLIStringArgument(QStringList const &args, QStringList const& paramNames) { + // go cli package is pretty permissive when it comes to parsing arguments. For each name 'param', all the following seems to be accepted: + // -param value + // --param value + // -param=value + // --param=value + + QStringList result; + qsizetype const argCount = args.count(); + for (qsizetype i = 0; i < args.size(); ++i) { + for (QString const ¶mName: paramNames) { + if ((i < argCount - 1) && ((args[i] == "-" + paramName) || (args[i] == "--" + paramName))) { + result.append(args[i + 1]); + i += 1; + continue; + } + if (QRegularExpressionMatch match = QRegularExpression(QString("^-{1,2}%1=(.+)$").arg(paramName)).match(args[i]); match.hasMatch()) { + result.append(match.captured(1)); + continue; + } + } + } + + return result; +} + +//**************************************************************************************************************************************************** +/// \param[in] argc The number of command-line arguments. +/// \param[in] argv The list of command-line arguments. +/// \return A QStringList representing the arguments list. +//**************************************************************************************************************************************************** +QStringList cliArgsToStringList(int argc, char **argv) { + QStringList result; + result.reserve(argc); + for (qsizetype i = 0; i < argc; ++i) { + result.append(QString::fromLocal8Bit(argv[i])); + } + return result; +} + } // namespace bridgepp diff --git a/internal/frontend/bridge-gui/bridgepp/bridgepp/CLI/CLIUtils.h b/internal/frontend/bridge-gui/bridgepp/bridgepp/CLI/CLIUtils.h index ecb45e66..fd7600ca 100644 --- a/internal/frontend/bridge-gui/bridgepp/bridgepp/CLI/CLIUtils.h +++ b/internal/frontend/bridge-gui/bridgepp/bridgepp/CLI/CLIUtils.h @@ -15,18 +15,15 @@ // You should have received a copy of the GNU General Public License // along with Proton Mail Bridge. If not, see . - #ifndef BRIDGEPP_CLI_UTILS_H #define BRIDGEPP_CLI_UTILS_H - namespace bridgepp { - QStringList stripStringParameterFromCommandLine(QString const ¶mName, QStringList const &commandLineParams); ///< Remove a string parameter from a list of command-line parameters. - +QStringList parseGoCLIStringArgument(QStringList const &args, QStringList const ¶mNames); ///< Parse a command-line string argument as expected by go's CLI package. +QStringList cliArgsToStringList(int argc, char **argv); ///< Converts C-style command-line arguments to a string list. } - #endif // BRIDGEPP_CLI_UTILS_H