diff --git a/apps/app-launch-helper/src/main.cpp b/apps/app-launch-helper/src/main.cpp index 34451334..ab219b6c 100644 --- a/apps/app-launch-helper/src/main.cpp +++ b/apps/app-launch-helper/src/main.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: LGPL-3.0-or-later @@ -6,15 +6,52 @@ #include "types.h" #include "variantValue.h" #include +#include #include -#include #include -#include +#include #include #include namespace { +// systemd escape rules: +// https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html#Specifiers +// https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html#Command%20lines +// But we only escape: +// 1. $ +// 2. An argument solely consisting of ";" +// '%' is not needed, see: +// https://github.com/systemd/systemd/blob/eefb46c83b130ccec16891c3dd89aa4f32229e80/src/core/dbus-execute.c#L1769 +void encodeArgument(std::string &arg) +{ + if (arg == ";") { + arg = R"(\;)"; + return; + } + + auto extra = std::count(arg.cbegin(), arg.cend(), '$'); + if (extra == 0) { + return; + } + + const auto oldSize = arg.size(); + const auto newSize = oldSize + extra; + arg.resize(newSize); + + auto i{oldSize}; + auto j{newSize}; + while (i > 0) { + const auto c = arg[--i]; + if (c == '$') { + arg[--j] = '$'; + arg[--j] = '$'; + } else { + arg[--j] = c; + } + } +} + ExitCode fromString(std::string_view str) { if (str == "done") { @@ -41,8 +78,8 @@ ExitCode fromString(const char *str) if (str == nullptr) { return ExitCode::Waiting; } - const std::string tmp{str}; - return fromString(tmp); + + return fromString(std::string_view{str}); } [[noreturn]] void releaseRes(sd_bus_error &error, msg_ptr &msg, bus_ptr &bus, ExitCode ret) @@ -54,8 +91,14 @@ ExitCode fromString(const char *str) std::exit(static_cast(ret)); } -int processExecStart(msg_ptr &msg, const std::deque &execArgs) +int processExecStart(msg_ptr msg, + std::vector::const_iterator begin, + std::vector::const_iterator end) { + if (begin == end) { + return -EINVAL; + } + int ret{0}; if (ret = sd_bus_message_open_container(msg, SD_BUS_TYPE_STRUCT, "sv"); ret < 0) { @@ -83,7 +126,9 @@ int processExecStart(msg_ptr &msg, const std::deque &execArgs) return ret; } - if (ret = sd_bus_message_append(msg, "s", execArgs[0].data()); ret < 0) { + std::string buffer{*begin}; + encodeArgument(buffer); + if (ret = sd_bus_message_append(msg, "s", buffer.c_str()); ret < 0) { sd_journal_perror("append binary of execStart failed."); return ret; } @@ -93,8 +138,11 @@ int processExecStart(msg_ptr &msg, const std::deque &execArgs) return ret; } - for (const auto &execArg : execArgs) { - if (ret = sd_bus_message_append(msg, "s", execArg.data()); ret < 0) { + for (auto it = begin; it != end; ++it) { + buffer = *it; + encodeArgument(buffer); + sd_journal_print(LOG_INFO, "after encode: %s", buffer.c_str()); + if (ret = sd_bus_message_append(msg, "s", buffer.c_str()); ret < 0) { sd_journal_perror("append args of execStart failed."); return ret; } @@ -136,223 +184,205 @@ int processExecStart(msg_ptr &msg, const std::deque &execArgs) DBusValueType getPropType(std::string_view key) { - static std::unordered_map map{{"Environment", DBusValueType::ArrayOfString}, - {"UnsetEnvironment", DBusValueType::ArrayOfString}, - {"WorkingDirectory", DBusValueType::String}, - {"ExecSearchPath", DBusValueType::ArrayOfString}}; - - if (const auto it = map.find(key); it != map.cend()) { - return it->second; + struct Entry + { + std::string_view k; + DBusValueType v; + }; + + // small data just use array and linear search + static constexpr std::array map{Entry{"Environment", DBusValueType::ArrayOfString}, + Entry{"UnsetEnvironment", DBusValueType::ArrayOfString}, + Entry{"ExecSearchPath", DBusValueType::ArrayOfString}, + Entry{"WorkingDirectory", DBusValueType::String}}; + + for (const auto &entry : map) { + if (entry.k == key) { + return entry.v; + } } - return DBusValueType::String; // fallback to string + return DBusValueType::String; } int appendPropValue(msg_ptr &msg, DBusValueType type, const std::vector &value) { - int ret{0}; - auto handler = creatValueHandler(msg, type); - if (handler == nullptr) { - sd_journal_perror("unknown type of property's variant."); - return -1; - } - - if (ret = handler->openVariant(); ret < 0) { - sd_journal_perror("open property's variant value failed."); - return ret; - } - - for (const auto &v : value) { - if (ret = handler->appendValue(std::string{v}); ret < 0) { - sd_journal_perror("append property's variant value failed."); - return ret; - } - } - - if (ret = handler->closeVariant(); ret < 0) { - sd_journal_perror("close property's variant value failed."); - return ret; - } - - return 0; -} + return std::visit( + [&value](auto &impl) { + if constexpr (std::is_same_v, std::monostate>) { + sd_journal_perror("unknown type of property's variant."); + return -1; + } else { + auto ret = impl.openVariant(); + if (ret < 0) { + sd_journal_perror("open property's variant value failed."); + return ret; + } -int processKVPair(msg_ptr &msg, std::unordered_map> &props) -{ - int ret{0}; - if (!props.empty()) { - for (auto &[key, value] : props) { - if (key == "ExecSearchPath") { - std::vector normalizedValue; for (const auto &v : value) { - const std::filesystem::path p{std::string{v}}; - if (!p.is_absolute()) { - sd_journal_print(LOG_INFO, "ExecSearchPath ignoring relative path: %s", std::string{v}.c_str()); - continue; + ret = impl.appendValue(v); + if (ret < 0) { + return ret; } - normalizedValue.emplace_back(p.lexically_normal().string()); } - if (normalizedValue.empty()) { - sd_journal_print(LOG_WARNING, "ExecSearchPath normalized to empty, skipping property"); - continue; + ret = impl.closeVariant(); + if (ret < 0) { + sd_journal_perror("close property's variant value failed."); + return ret; } - value = std::move(normalizedValue); + return 0; } + }, + handler); +} - const std::string keyStr{key}; - if (ret = sd_bus_message_open_container(msg, SD_BUS_TYPE_STRUCT, "sv"); ret < 0) { - sd_journal_perror("open struct of properties failed."); - return ret; - } +int processKVPair(msg_ptr msg, std::unordered_map> &props) +{ + int ret{0}; + for (auto &[key, value] : props) { + if (ret = sd_bus_message_open_container(msg, SD_BUS_TYPE_STRUCT, "sv"); ret < 0) { + sd_journal_perror("open struct of properties failed."); + return ret; + } - if (ret = sd_bus_message_append(msg, "s", keyStr.c_str()); ret < 0) { - sd_journal_perror("append key of property failed."); - return ret; - } + sd_journal_print(LOG_INFO, "key:%s", key.data()); + if (ret = sd_bus_message_append(msg, "s", key.c_str()); ret < 0) { + sd_journal_perror("append key of property failed."); + return ret; + } - if (ret = appendPropValue(msg, getPropType(key), value); ret < 0) { - sd_journal_perror("append value of property failed."); - return ret; - } + if (ret = appendPropValue(msg, getPropType(key), value); ret < 0) { + sd_journal_perror("append value of property failed."); + return ret; + } - if (ret = sd_bus_message_close_container(msg); ret < 0) { - sd_journal_perror("close struct of properties failed."); - return ret; - } + if (ret = sd_bus_message_close_container(msg); ret < 0) { + sd_journal_perror("close struct of properties failed."); + return ret; } } + return 0; } -std::string cmdParse(msg_ptr &msg, std::deque cmdLines) +std::optional cmdParse(msg_ptr &msg, const std::vector &cmdLines) { - std::string serviceName{"internalError"}; - std::unordered_map> props; - - while (!cmdLines.empty()) { // NOTE: avoid stl exception - auto str = cmdLines.front(); - if (str.size() < 2) { - sd_journal_print(LOG_WARNING, "invalid option %s.", str.data()); - cmdLines.pop_front(); + std::string unitName; + std::unordered_map> props; + + size_t cursor{0}; + const auto total{cmdLines.size()}; + while (cursor < total) { + auto str = cmdLines[cursor]; + if (str == "--") { + ++cursor; + break; + } + + if (str.size() < 3 || str.compare(0, 2, "--") != 0) { + sd_journal_print(LOG_WARNING, "Unknown option: %s", str.data()); + return std::nullopt; + } + + ++cursor; + auto kvStr = str.substr(2); + auto eqPos = kvStr.find('='); + + if (eqPos == std::string_view::npos || eqPos == 0) { + sd_journal_print(LOG_WARNING, "invalid k-v pair: %s", kvStr.data()); + return std::nullopt; + } + + std::string key{kvStr.substr(0, eqPos)}; + if (key == "Type") { + // NOTE: + // Systemd service type must be "exec", + // this should not be configured in command line arguments. + sd_journal_print(LOG_WARNING, "Type should not be configured in command line arguments."); continue; } - if (str.substr(0, 2) != "--") { - sd_journal_print(LOG_INFO, "unknown option %s.", str.data()); - cmdLines.pop_front(); + + auto value = kvStr.substr(eqPos + 1); + if (key == "unitName") { + unitName = value; continue; } - auto kvStr = str.substr(2); - if (!kvStr.empty()) { - const auto *it = kvStr.cbegin(); - if (it = std::find(it, kvStr.cend(), '='); it == kvStr.cend()) { - sd_journal_print(LOG_WARNING, "invalid k-v pair: %s", kvStr.data()); - cmdLines.pop_front(); - continue; - } - auto splitIndex = std::distance(kvStr.cbegin(), it); - if (++it == kvStr.cend()) { - sd_journal_print(LOG_WARNING, "invalid k-v pair: %s", kvStr.data()); - cmdLines.pop_front(); - continue; - } + if (key == "ExecSearchPath") { + const std::filesystem::path path{value}; - auto key = kvStr.substr(0, splitIndex); - if (key == "Type") { - // NOTE: - // Systemd service type must be "exec", - // this should not be configured in command line arguments. - cmdLines.pop_front(); + if (!path.is_absolute()) { + sd_journal_print(LOG_WARNING, "ExecSearchPath ignoring relative path: %s", value.data()); continue; } - props[key].emplace_back(kvStr.substr(splitIndex + 1)); - cmdLines.pop_front(); + props[std::move(key)].emplace_back(path.lexically_normal()); continue; } - cmdLines.pop_front(); // NOTE: skip "--" - break; + props[std::move(key)].emplace_back(value); } // Processing of the binary file and its parameters that am want to launch - const auto &execArgs = cmdLines; - if (execArgs.empty()) { - sd_journal_print(LOG_ERR, "param exec is empty."); - serviceName = "invalidInput"; - return serviceName; - } - if (props.find("unitName") == props.cend()) { - sd_journal_perror("unitName doesn't exists."); - serviceName = "invalidInput"; - return serviceName; + if (unitName.empty() || cursor >= total) { + sd_journal_print(LOG_ERR, "Missing unitName or execution arguments."); + return "invalidInput"; } int ret{0}; - if (ret = sd_bus_message_append(msg, "s", props["unitName"].front().c_str()); ret < 0) { // unitName + if (ret = sd_bus_message_append(msg, "ss", unitName.c_str(), "replace"); ret < 0) { // unitName and start mode sd_journal_perror("append unitName failed."); - return serviceName; - } - - serviceName = props["unitName"].front(); - props.erase("unitName"); - - if (ret = sd_bus_message_append(msg, "s", "replace"); ret < 0) { // start mode - sd_journal_perror("append startMode failed."); - return serviceName; + return std::nullopt; } // process properties: a(sv) if (ret = sd_bus_message_open_container(msg, SD_BUS_TYPE_ARRAY, "(sv)"); ret < 0) { sd_journal_perror("open array failed."); - return serviceName; - } - - if (ret = sd_bus_message_append(msg, "(sv)", "Type", "s", "exec"); ret < 0) { - sd_journal_perror("append type failed."); - return serviceName; - } - - if (ret = sd_bus_message_append(msg, "(sv)", "ExitType", "s", "cgroup"); ret < 0) { - sd_journal_perror("append exit type failed."); - return serviceName; - } - - if (ret = sd_bus_message_append(msg, "(sv)", "Slice", "s", "app.slice"); ret < 0) { - sd_journal_perror("append application slice failed."); - return serviceName; - } - - if (ret = sd_bus_message_append(msg, "(sv)", "CollectMode", "s", "inactive-or-failed"); ret < 0) { - sd_journal_perror("append application slice failed."); - return serviceName; + return std::nullopt; + } + + if (ret = sd_bus_message_append(msg, + "(sv)(sv)(sv)(sv)", + "Type", + "s", + "exec", + "ExitType", + "s", + "cgroup", + "Slice", + "s", + "app.slice", + "CollectMode", + "s", + "inactive-or-failed"); + ret < 0) { + sd_journal_perror("failed to append necessary properties."); + return std::nullopt; } if (ret = processKVPair(msg, props); ret < 0) { // process props - serviceName = "invalidInput"; - return serviceName; + return std::nullopt; } - if (ret = processExecStart(msg, execArgs); ret < 0) { - serviceName = "invalidInput"; - return serviceName; + if (ret = processExecStart(msg, cmdLines.cbegin() + cursor, cmdLines.cend()); ret < 0) { + return std::nullopt; } if (ret = sd_bus_message_close_container(msg); ret < 0) { sd_journal_perror("close array failed."); - return serviceName; + return std::nullopt; } // append aux, it's unused for now if (ret = sd_bus_message_append(msg, "a(sa(sv))", 0); ret < 0) { sd_journal_perror("append aux failed."); - return serviceName; + return std::nullopt; } - return serviceName; + return unitName; } int jobRemovedReceiver(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) @@ -386,7 +416,7 @@ int process_dbus_message(sd_bus *bus) int ret{0}; ret = sd_bus_process(bus, nullptr); if (ret < 0) { - sd_journal_print(LOG_ERR, "event loop error."); + sd_journal_perror("event loop error."); return ret; } @@ -410,7 +440,6 @@ int main(int argc, const char *argv[]) sd_bus_error error{SD_BUS_ERROR_NULL}; sd_bus_message *msg{nullptr}; sd_bus *bus{nullptr}; - std::string serviceId; int ret{0}; if (ret = sd_bus_open_user(&bus); ret < 0) { @@ -425,20 +454,19 @@ int main(int argc, const char *argv[]) releaseRes(error, msg, bus, ExitCode::InternalError); } - std::deque args; + std::vector args; + args.reserve(argc); for (int i = 1; i < argc; ++i) { args.emplace_back(argv[i]); } - serviceId = cmdParse(msg, std::move(args)); - if (serviceId == "internalError") { + auto serviceId = cmdParse(msg, args); + if (!serviceId) { releaseRes(error, msg, bus, ExitCode::InternalError); - } else if (serviceId == "invalidInput") { - releaseRes(error, msg, bus, ExitCode::InvalidInput); } const char *path{nullptr}; - JobRemoveResult resultData{serviceId}; + JobRemoveResult resultData{serviceId.value()}; if (ret = sd_bus_match_signal( bus, nullptr, SystemdService, SystemdObjectPath, SystemdInterfaceName, "JobRemoved", jobRemovedReceiver, &resultData); @@ -453,7 +481,7 @@ int main(int argc, const char *argv[]) sd_journal_print(LOG_ERR, "failed to call StartTransientUnit: [%s,%s]", error.name, error.message); releaseRes(error, msg, bus, ExitCode::InternalError); } else { - sd_journal_print(LOG_INFO, "call StartTransientUnit successfully, service ID: %s", serviceId.c_str()); + sd_journal_print(LOG_INFO, "call StartTransientUnit successfully, service ID: %s", serviceId->c_str()); } if (ret = sd_bus_message_read(reply, "o", &path); ret < 0) { diff --git a/apps/app-launch-helper/src/types.h b/apps/app-launch-helper/src/types.h index 4475243f..7f5be478 100644 --- a/apps/app-launch-helper/src/types.h +++ b/apps/app-launch-helper/src/types.h @@ -1,18 +1,17 @@ -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: LGPL-3.0-or-later #ifndef TYPES_H #define TYPES_H +#include #include #include -#include -#include -enum class ExitCode { SystemdError = -3, InvalidInput = -2, InternalError = -1, Done = 0, Waiting = 1 }; +enum class ExitCode : int8_t { SystemdError = -3, InvalidInput = -2, InternalError = -1, Done = 0, Waiting = 1 }; -enum class DBusValueType { String, ArrayOfString }; +enum class DBusValueType : uint8_t { String, ArrayOfString }; using msg_ptr = sd_bus_message *; using bus_ptr = sd_bus *; diff --git a/apps/app-launch-helper/src/variantValue.h b/apps/app-launch-helper/src/variantValue.h index bd269c74..cc206ceb 100644 --- a/apps/app-launch-helper/src/variantValue.h +++ b/apps/app-launch-helper/src/variantValue.h @@ -1,50 +1,43 @@ -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: LGPL-3.0-or-later +#ifndef VARIANTVALUE_H +#define VARIANTVALUE_H + #include "types.h" -#include +#include -class VariantValue +class StringHandler { public: - explicit VariantValue(msg_ptr &msg) + explicit StringHandler(msg_ptr msg) : m_msg(msg) { } - virtual ~VariantValue() = default; - VariantValue(const VariantValue &) = delete; - VariantValue(VariantValue &&) = delete; - VariantValue &operator=(const VariantValue &) = delete; - VariantValue &operator=(VariantValue &&) = delete; - - virtual int openVariant() noexcept = 0; - virtual int closeVariant() noexcept = 0; - virtual int appendValue(std::string &&value) noexcept = 0; - - msg_ptr &msgRef() noexcept { return m_msg; } + int openVariant() noexcept; + int closeVariant() noexcept; + int appendValue(std::string_view value) noexcept; private: - msg_ptr &m_msg; + msg_ptr m_msg; }; -class StringValue : public VariantValue +class ASHandler { - using VariantValue::VariantValue; - public: - int openVariant() noexcept override; - int closeVariant() noexcept override; - int appendValue(std::string &&value) noexcept override; -}; - -class ASValue : public VariantValue -{ - using VariantValue::VariantValue; + explicit ASHandler(msg_ptr msg) + : m_msg(msg) + { + } + int openVariant() noexcept; + int closeVariant() noexcept; + int appendValue(std::string_view value) noexcept; -public: - int openVariant() noexcept override; - int closeVariant() noexcept override; - int appendValue(std::string &&value) noexcept override; +private: + msg_ptr m_msg; }; -std::unique_ptr creatValueHandler(msg_ptr &msg, DBusValueType type); +using Handler = std::variant; +Handler creatValueHandler(msg_ptr msg, DBusValueType type) noexcept; + +#endif diff --git a/apps/app-launch-helper/src/variantvalue.cpp b/apps/app-launch-helper/src/variantvalue.cpp index 73c4d9bb..d54ad47c 100644 --- a/apps/app-launch-helper/src/variantvalue.cpp +++ b/apps/app-launch-helper/src/variantvalue.cpp @@ -1,55 +1,55 @@ -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: LGPL-3.0-or-later #include "variantValue.h" -#include "constant.h" -#include -std::unique_ptr creatValueHandler(msg_ptr &msg, DBusValueType type) +Handler creatValueHandler(msg_ptr msg, DBusValueType type) noexcept { switch (type) { case DBusValueType::String: - return std::make_unique(msg); + return StringHandler{msg}; case DBusValueType::ArrayOfString: - return std::make_unique(msg); + return ASHandler{msg}; default: - return nullptr; + return std::monostate{}; } } -int StringValue::openVariant() noexcept +int StringHandler::openVariant() noexcept { - return sd_bus_message_open_container(msgRef(), SD_BUS_TYPE_VARIANT, "s"); + return sd_bus_message_open_container(m_msg, SD_BUS_TYPE_VARIANT, "s"); } -int StringValue::closeVariant() noexcept +int StringHandler::closeVariant() noexcept { - return sd_bus_message_close_container(msgRef()); + return sd_bus_message_close_container(m_msg); } -int StringValue::appendValue(std::string &&value) noexcept +int StringHandler::appendValue(std::string_view value) noexcept { - return sd_bus_message_append(msgRef(), "s", value.data()); + return sd_bus_message_append(m_msg, "s", value.data()); } -int ASValue::openVariant() noexcept +int ASHandler::openVariant() noexcept { - if (int ret = sd_bus_message_open_container(msgRef(), SD_BUS_TYPE_VARIANT, "as"); ret < 0) + if (const auto ret = sd_bus_message_open_container(m_msg, SD_BUS_TYPE_VARIANT, "as"); ret < 0) { return ret; + } - return sd_bus_message_open_container(msgRef(), SD_BUS_TYPE_ARRAY, "s"); + return sd_bus_message_open_container(m_msg, SD_BUS_TYPE_ARRAY, "s"); } -int ASValue::closeVariant() noexcept +int ASHandler::closeVariant() noexcept { - if (int ret = sd_bus_message_close_container(msgRef()); ret < 0) + if (const auto ret = sd_bus_message_close_container(m_msg); ret < 0) { return ret; + } - return sd_bus_message_close_container(msgRef()); + return sd_bus_message_close_container(m_msg); } -int ASValue::appendValue(std::string &&value) noexcept +int ASHandler::appendValue(std::string_view value) noexcept { - return sd_bus_message_append(msgRef(), "s", value.data()); + return sd_bus_message_append(m_msg, "s", value.data()); } diff --git a/apps/dde-application-manager/src/main.cpp b/apps/dde-application-manager/src/main.cpp index 258f3af7..5b72f7a1 100644 --- a/apps/dde-application-manager/src/main.cpp +++ b/apps/dde-application-manager/src/main.cpp @@ -1,16 +1,13 @@ -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: LGPL-3.0-or-later +#include "applicationmanagerstorage.h" +#include "cgroupsidentifier.h" +#include "dbus/applicationmanager1service.h" #include "global.h" #include #include -#include -#include "dbus/applicationmanager1service.h" -#include "cgroupsidentifier.h" -#include "applicationmanagerstorage.h" -#include -#include Q_LOGGING_CATEGORY(DDEAMProf, "dde.am.prof", QtInfoMsg) diff --git a/src/dbus/applicationservice.cpp b/src/dbus/applicationservice.cpp index 5d22a022..f4bdc436 100644 --- a/src/dbus/applicationservice.cpp +++ b/src/dbus/applicationservice.cpp @@ -1,10 +1,9 @@ -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: LGPL-3.0-or-later #include "dbus/applicationservice.h" #include "APPobjectmanager1adaptor.h" -#include "applicationadaptor.h" #include "applicationchecker.h" #include "applicationmanagerstorage.h" #include "config.h" @@ -19,6 +18,7 @@ #include "propertiesForwarder.h" #include #include +#include #include #include #include @@ -35,11 +35,12 @@ #include #include #include -#include using namespace Qt::Literals::StringLiterals; -static inline void appendEnvs(const QVariant &var, QStringList &envs) +namespace { + +void appendEnvs(const QVariant &var, QStringList &envs) { if (var.canConvert()) { envs.append(var.value()); @@ -48,6 +49,35 @@ static inline void appendEnvs(const QVariant &var, QStringList &envs) } } +void unescapeEnvs(QVariantMap &options) noexcept +{ + if (options.constFind("env") == options.cend()) { + return; + } + QStringList result; + const auto &envsVar = options["env"]; + auto envs = envsVar.toStringList(); + for (const auto &var : std::as_const(envs)) { + if (var.startsWith(u"DSG_APP_ID="_s)) { + result << var; + continue; + } + wordexp_t p; + if (wordexp(var.toStdString().c_str(), &p, 0) == 0) { + for (size_t i = 0; i < p.we_wordc; i++) { + result << QString::fromLocal8Bit(p.we_wordv[i]); + } + wordfree(&p); + } else { + return; + } + } + + options.insert("env", result); +} + +} // namespace + void ApplicationService::appendExtraEnvironments(QVariantMap &runtimeOptions) const noexcept { DCORE_USE_NAMESPACE @@ -271,12 +301,7 @@ ApplicationService::Launch(const QString &action, const QStringList &fields, con auto optionsMap = options; appendExtraEnvironments(optionsMap); - if (!realExec.isNull()) { // we want to replace exec of this applications. - if (realExec.isEmpty()) { - qWarning() << "try to replace exec but it's empty."; - return {}; - } - + if (realExec.isEmpty()) { // we want to replace exec of this applications. execStr = realExec; } @@ -285,7 +310,7 @@ ApplicationService::Launch(const QString &action, const QStringList &fields, con optionsMap.insert(setWorkingPathLaunchOption::key(), workDir->toString()); } - while (execStr.isEmpty() and !action.isEmpty() and !supportedActions.isEmpty()) { // break trick + while (execStr.isEmpty() && !action.isEmpty() && !supportedActions.isEmpty()) { // break trick if (auto index = supportedActions.indexOf(action); index == -1) { qWarning() << "can't find " << action << " in supported actions List. application will use default action to launch."; break; @@ -308,7 +333,7 @@ ApplicationService::Launch(const QString &action, const QStringList &fields, con if (execStr.isEmpty()) { auto Actions = m_entry->value(DesktopFileEntryKey, "Exec"); if (!Actions) { - QString msg{"application can't be executed."}; + const QString msg{"application can't be executed."}; qWarning() << msg; safe_sendErrorReply(QDBusError::Failed, msg); return {}; @@ -316,7 +341,7 @@ ApplicationService::Launch(const QString &action, const QStringList &fields, con execStr = Actions.value().toString(); if (execStr.isEmpty()) { - QString msg{"maybe entry actions's format is invalid, abort launch."}; + const QString msg{"maybe entry actions's format is invalid, abort launch."}; qWarning() << msg; safe_sendErrorReply(QDBusError::Failed, msg); return {}; @@ -348,16 +373,23 @@ ApplicationService::Launch(const QString &action, const QStringList &fields, con // Those are internal properties, user shouldn't pass them to Application Manager optionsMap.remove("_autostart"); optionsMap.remove("_hooks"); + optionsMap.remove("_builtIn_searchExec"); if (const auto &hooks = parent()->applicationHooks(); !hooks.isEmpty()) { optionsMap.insert("_hooks", hooks); } optionsMap.insert("_builtIn_searchExec", parent()->systemdPathEnv()); processCompatibility(action, optionsMap, execStr); - unescapeEens(optionsMap); + unescapeEnvs(optionsMap); + + auto workingDir = optionsMap.value("path").toString(); + if (!workingDir.isEmpty() && QDir::isRelativePath(workingDir)) { + qWarning() << "relative working dir is not supported: " << workingDir; + workingDir.clear(); + } auto cmds = generateCommand(optionsMap); - auto task = unescapeExec(execStr, fields); + auto task = processExec(execStr, fields, workingDir); if (!task) { safe_sendErrorReply(QDBusError::InternalError, "Invalid Command."); return {}; @@ -372,91 +404,75 @@ ApplicationService::Launch(const QString &action, const QStringList &fields, con if (terminal()) { // don't change this sequence cmds.push_back("deepin-terminal"); - cmds.push_back("--keep-open"); // keep terminal open, prevent exit immediately + cmds.push_back("--keep-open"); cmds.push_back("-e"); // run all original execution commands in deepin-terminal } auto &jobManager = parent()->jobManager(); return jobManager.addJob( m_applicationPath.path(), - [this, task, cmds = std::move(cmds)](const QVariant &value) -> QVariant { // do not change it to mutable lambda - auto instanceRandomUUID = QUuid::createUuid().toString(QUuid::Id128); - auto objectPath = m_applicationPath.path() + "/" + instanceRandomUUID; - auto newCommands = cmds; - - if (value.isNull()) { - newCommands.push_front(QString{"--SourcePath=%1"}.arg(m_desktopSource.sourcePath())); - newCommands.push_front(QString{R"(--unitName=app-DDE-%1@%2.service)"}.arg( - escapeApplicationId(this->id()), instanceRandomUUID)); // launcher should use this instanceId - newCommands.append(task.command); - - QProcess process; - qDebug() << "launcher :" << m_launcher << "run with commands:" << newCommands; - process.start(m_launcher, newCommands); - process.waitForFinished(); - if (auto code = process.exitCode(); code != 0) { - qWarning() << "Launch Application Failed"; - return QDBusError::Failed; + [this, task = task, cmds = std::move(cmds)](const QVariant &value) mutable -> QVariant { + const auto instanceRandomUUID = QUuid::createUuid().toString(QUuid::Id128); + const auto objectPath = m_applicationPath.path() + "/" + instanceRandomUUID; + + QStringList newCommands; + const int estimatedSize = 5 + cmds.size() + task.command.size() + (value.isValid() ? 1 : 0); + newCommands.reserve(estimatedSize); + newCommands + << QStringLiteral("--unitName=app-DDE-%1@%2.service").arg(escapeApplicationId(this->id()), instanceRandomUUID); + newCommands << QStringLiteral("--SourcePath=%1").arg(m_desktopSource.sourcePath()); + newCommands << std::move(cmds); + + QStringList formattedRes; + if (!value.isNull()) { + QList urls; + if (value.canConvert>()) { + urls = std::move(value).value>(); + } else { + urls.append(std::move(value).value()); } - return objectPath; - } - - if (task.argNum != -1) { - if (task.argNum >= newCommands.size()) { - qCritical() << "task.argNum >= task.command.size()"; - return QDBusError::Failed; + formattedRes.reserve(urls.size()); + for (const auto &url : std::as_const(urls)) { + if (!task.local) { + formattedRes << url.toString(); + } else if (url.isLocalFile()) { + formattedRes << url.toLocalFile(); + } else { + // TODO: Remote file handling logic + qWarning() << "Remote file not supported yet, skipping:" << url; + } } + } - QStringList rawRes; - if (value.canConvert()) { // from %F, %U - rawRes = value.toStringList(); - } else if (value.canConvert()) { // from %f, %u - rawRes.append(value.toString()); - } else { - qWarning() << "value type mismatch:" << value; - return QDBusError::Failed; + for (int i = 0; i < task.command.size(); ++i) { + if (i != task.argNum) { + newCommands << std::move(task.command[i]); + continue; } - if (task.local) { - std::for_each(rawRes.begin(), rawRes.end(), [](QString &str) { - auto url = QUrl::fromUserInput(str); + auto targetArg = std::move(task.command[i]); + if (task.fieldLocation != -1) { + if (formattedRes.size() > 1) { + qWarning() << "multiple resources are found, only the first one will be used."; + } - if (url.isLocalFile()) { - str = url.toLocalFile(); - return; - } + targetArg.replace(task.fieldLocation, 2, formattedRes.isEmpty() ? QString{} : formattedRes.takeFirst()); + newCommands << std::move(targetArg); - // TODO: processing remote files - // str = downloadToLocal(url).toLocalFile(); - }); - } - - auto newCmds = task.command; - if (task.fieldLocation == -1) { // single field, e.g. "demo %U" - auto it = newCmds.begin() + task.argNum + 1; - std::for_each( - rawRes.rbegin(), rawRes.rend(), [&newCmds, &it](QString &str) { it = newCmds.insert(it, str); }); + if (!formattedRes.isEmpty()) { + newCommands << std::move(formattedRes); + } } else { - auto arg = newCmds.begin() + task.argNum + 1; - arg->insert(task.fieldLocation, rawRes.takeFirst()); - ++arg; - - // expand the rest of res - std::for_each( - rawRes.rbegin(), rawRes.rend(), [&newCmds, &arg](QString &str) { arg = newCmds.insert(arg, str); }); + newCommands << std::move(formattedRes); } - - newCommands.append(std::move(newCmds)); } - newCommands.push_front(QString{"--SourcePath=%1"}.arg(m_desktopSource.sourcePath())); - newCommands.push_front( - QString{R"(--unitName=app-DDE-%1@%2.service)"}.arg(escapeApplicationId(this->id()), instanceRandomUUID)); - QProcess process; - qDebug().noquote() << "launcher :" << m_launcher << "run with commands:" << newCommands; - process.start(getApplicationLauncherBinary(), newCommands); + const auto &bin = getApplicationLauncherBinary(); + qDebug().noquote() << "Launcher path:" << bin << ", Run with commands:" << newCommands; + + process.start(bin, newCommands); process.waitForFinished(); auto exitCode = process.exitCode(); if (exitCode != 0) { @@ -1048,70 +1064,79 @@ void ApplicationService::resetEntry(DesktopEntry *newEntry) noexcept emit xCreatedByChanged(); } -std::optional ApplicationService::unescapeExecArgs(const QString &str) noexcept +std::optional ApplicationService::splitExecArguments(QStringView str) noexcept { - auto unescapedStr = unescape(str, true); - if (unescapedStr.isEmpty()) { - qWarning() << "unescape Exec failed."; + if (str.isEmpty()) { return std::nullopt; } - // Escape $ characters to prevent wordexp from interpreting them as special sequences - auto escapedForWordexp = escapeForWordexp(unescapedStr); + QStringList args; + QString currentToken; + currentToken.reserve(str.size()); - auto deleter = [](wordexp_t *word) { - wordfree(word); - delete word; - }; + bool inQuotes{false}; + bool hasToken{false}; - std::unique_ptr words{new (std::nothrow) wordexp_t{0, nullptr, 0}, deleter}; - if (words == nullptr) { - qCritical() << "couldn't new wordexp_t"; - return std::nullopt; - } + for (const auto *it = str.begin(); it != str.end(); ++it) { + const auto c = *it; + + if (c == u'\\') { + if ((it + 1) == str.end()) { + qWarning() << "Exec parsing error: Backslash at end of line"; + return std::nullopt; + } - if (auto ret = wordexp(escapedForWordexp.toLocal8Bit(), words.get(), WRDE_SHOWERR); ret != 0) { - if (ret != 0) { - QString errMessage; - switch (ret) { - case WRDE_BADCHAR: - errMessage = "BADCHAR"; - break; - case WRDE_BADVAL: - errMessage = "BADVAL"; - break; - case WRDE_CMDSUB: - errMessage = "CMDSUB"; - break; - case WRDE_NOSPACE: - errMessage = "NOSPACE"; - break; - case WRDE_SYNTAX: - errMessage = "SYNTAX"; - break; - default: - errMessage = "unknown"; + const auto next = *(++it); + + if (inQuotes) { + if (next == u'"' || next == u'\\' || next == u'$' || next == u'`') { + currentToken.append(next); + } else { + currentToken.append(u'\\'); + currentToken.append(next); + } + } else { + currentToken.append(next); } - qWarning() << "wordexp error: " << errMessage; - return std::nullopt; + hasToken = true; + continue; + } + + if (c == u'"') { + inQuotes = !inQuotes; + hasToken = true; + continue; } + + if (c.isSpace() && !inQuotes) { + if (hasToken) { + args.append(currentToken); + currentToken.clear(); + hasToken = false; + } + } else { + currentToken.append(c); + hasToken = true; + } + } + + if (inQuotes) { + qWarning() << "Exec parsing error: Unterminated double quote"; + return std::nullopt; } - QStringList execList; - for (std::size_t i = 0; i < words->we_wordc; ++i) { - execList.emplace_back(words->we_wordv[i]); + if (hasToken) { + args.append(currentToken); } - return execList; + return args; } -LaunchTask ApplicationService::unescapeExec(const QString &str, QStringList fields) noexcept +LaunchTask ApplicationService::processExec(const QString &str, const QStringList& fields, const QString& dir) noexcept { - LaunchTask task; - auto args = unescapeExecArgs(str); - + auto args = splitExecArguments(str); if (!args) { - qWarning() << "unescapeExecArgs failed."; + qWarning() << "splitExecArguments failed."; return {}; } @@ -1120,82 +1145,74 @@ LaunchTask ApplicationService::unescapeExec(const QString &str, QStringList fiel return {}; } + qDebug() << "splitExecArguments:" << *args; + + LaunchTask task; task.LaunchBin = args->first(); - const QChar percentage{'%'}; - bool exclusiveField{false}; + task.command.reserve(args->size() + 2); // 2 for icon - for (auto arg = args->begin(); arg != args->end(); ++arg) { - QString newArg; + const QChar percentage{u'%'}; + bool exclusiveField{false}; - for (const auto *it = arg->cbegin(); it != arg->cend();) { - if (*it != percentage) { - newArg.append(*(it++)); - continue; - } + for (auto &rawArg : *args) { + if (!rawArg.contains(percentage)) { + task.command.append(std::move(rawArg)); + continue; + } - const auto *code = it + 1; - if (code == arg->cend()) { - qWarning() << R"(content of exec is invalid, a unterminated % is detected.)"; - return {}; - } + QString processedArg; + processedArg.reserve(rawArg.size()); + bool dynamicField{false}; - if (*code == percentage) { - newArg.append(percentage); - it += 2; + for (qsizetype j = 0; j < rawArg.size(); ++j) { + const auto ch = rawArg[j]; + if (ch != percentage || j + 1 >= rawArg.size()) { + processedArg.append(ch); continue; } - switch (auto c = code->toLatin1(); c) { - case 'f': + const auto code = rawArg[++j]; + switch (code.unicode()) { + case u'f': + case u'F': + case u'u': [[fallthrough]]; - case 'F': { // Defer to async job + case u'U': { if (exclusiveField) { - qDebug() << QString{"exclusive field is detected again, %%1 will be ignored."}.arg(c); + qDebug() << QString{"exclusive field is detected again, %%1 will be ignored."}.arg(code); break; } exclusiveField = true; if (fields.empty()) { - qDebug() << QString{"fields is empty, %%1 will be ignored."}.arg(c); + qDebug() << QString{"fields is empty, %%1 will be ignored."}.arg(code); break; } - if (c == 'F') { - task.Resources.emplace_back(fields); - } else { - std::for_each( - fields.begin(), fields.end(), [&task](QString &str) { task.Resources.emplace_back(std::move(str)); }); - } + dynamicField = true; + task.argNum = task.command.size(); + task.fieldLocation = processedArg.size(); + task.local = (code.toLower() == u'f'); - task.argNum = std::distance(args->begin(), arg) - 1; - task.fieldLocation = std::distance(arg->cbegin(), it) - 1; - task.local = true; - } break; - case 'u': - case 'U': { - if (exclusiveField) { - qDebug() << QString{"exclusive field is detected again, %%1 will be ignored."}.arg(c); - break; + // respect the original url, replace it to exec directly + QList resources; + resources.reserve(fields.size()); + for (const auto& field : fields) { + resources.emplace_back(QUrl::fromUserInput(field, dir, QUrl::AssumeLocalFile)); } - exclusiveField = true; - if (fields.empty()) { - qDebug() << QString{"fields is empty, %%1 will be ignored."}.arg(c); - break; - } - - // respect the original url, pass it to exec directly - if (c == 'U') { - task.Resources.emplace_back(std::move(fields)); + if (code.isUpper()) { + task.Resources.emplace_back(std::in_place_type>, std::move(resources)); } else { - std::for_each( - fields.begin(), fields.end(), [&task](QString &url) { task.Resources.emplace_back(std::move(url)); }); + task.Resources.reserve(fields.size()); + for (auto &&resource : resources) { + task.Resources.emplace_back(std::in_place_type, std::move(resource)); + } } - task.argNum = std::distance(args->begin(), arg) - 1; - task.fieldLocation = std::distance(arg->cbegin(), it) - 1; + processedArg.append(percentage).append(code); } break; - case 'i': { + case u'i': { auto val = m_entry->value(DesktopFileEntryKey, "Icon"); if (!val) { qDebug() << R"(Application Icons can't be found. %i will be ignored.)"; @@ -1208,10 +1225,16 @@ LaunchTask ApplicationService::unescapeExec(const QString &str, QStringList fiel break; } - // split at the end of loop - newArg.append(QString{"--icon %1"}.arg(iconStr)); + if (!processedArg.isEmpty()) { + task.command.append(std::move(processedArg)); + processedArg.clear(); + } + + // spec says: + // The Icon key of the desktop entry expanded as two arguments, first --icon and then the value of the Icon key. + task.command << QStringLiteral("--icon") << std::move(iconStr); } break; - case 'c': { + case u'c': { auto val = m_entry->value(DesktopFileEntryKey, "Name"); if (!val) { qDebug() << R"(Application Name can't be found. %c will be ignored.)"; @@ -1230,41 +1253,34 @@ LaunchTask ApplicationService::unescapeExec(const QString &str, QStringList fiel break; } - newArg.append(NameStr); + processedArg.append(NameStr); + } break; + case u'k': { + processedArg.append(m_desktopSource.sourcePath()); } break; - case 'k': { // ignore all desktop file location for now. - newArg.append(m_desktopSource.sourcePath()); + case u'%': { + processedArg.append(percentage); } break; - case 'd': - case 'D': - case 'n': - case 'N': - case 'v': + case u'd': + case u'D': + case u'n': + case u'N': + case u'v': [[fallthrough]]; // Deprecated field codes should be removed from the command line and ignored. - case 'm': { - qDebug() << "field code" << *code << "has been deprecated."; + case u'm': { + qWarning() << "field code" << code << "has been deprecated."; } break; default: { - qDebug() << "unknown field code:" << *code << ", ignore it."; + // spec says: + // Command lines that contain a field code that is not listed in this specification are invalid and MUST NOT be processed + qCritical() << "unknown field code:" << code << ", Invalid."; + return {}; } } - - it += 2; // skip filed code - } - - // unescapeExecArgs()函数中使用了遵循POSIX标准的wordexp,可以正确解析被引号包裹的整体,解析成参数列表, - // 被分割的都是一个个整体,若分割后的参数在这里还能再按空格分割, - // 则说明这个参数被包裹在一个引号中作为了一个整体,此时我们不能再分割这个参数 - const bool noSplit = (*arg).split(' ').size() > 1; - QStringList newArgList; - if (noSplit) { - newArgList = {newArg}; - } else { - newArgList = newArg.split(' ', Qt::SkipEmptyParts); } - if (!newArgList.isEmpty()) { - task.command.append(std::move(newArgList)); + if (dynamicField || !processedArg.isEmpty()) { + task.command.append(std::move(processedArg)); } } @@ -1272,37 +1288,10 @@ LaunchTask ApplicationService::unescapeExec(const QString &str, QStringList fiel task.Resources.emplace_back(QVariant{}); // mapReduce should run once at least } - qInfo() << "after unescape exec:" << task.LaunchBin << task.command << task.Resources; + qDebug() << "Parsed Exec:" << task.LaunchBin << "Cmds:" << task.command; return task; } -void ApplicationService::unescapeEens(QVariantMap &options) noexcept -{ - if (options.constFind("env") == options.cend()) { - return; - } - QStringList result; - const auto &envsVar = options["env"]; - auto envs = envsVar.toStringList(); - for (const auto &var : std::as_const(envs)) { - if (var.startsWith(u"DSG_APP_ID="_s)) { - result << var; - continue; - } - wordexp_t p; - if (wordexp(var.toStdString().c_str(), &p, 0) == 0) { - for (size_t i = 0; i < p.we_wordc; i++) { - result << QString::fromLocal8Bit(p.we_wordv[i]); // 将结果转换为QString - } - wordfree(&p); - } else { - return; - } - } - - options.insert("env", result); -} - QVariant ApplicationService::findEntryValue(const QString &group, const QString &valueKey, EntryValueType type, @@ -1393,3 +1382,4 @@ void ApplicationService::setAutostartSource(AutostartSource &&source) noexcept m_autostartSource = std::move(source); emit autostartChanged(); } + diff --git a/src/dbus/applicationservice.h b/src/dbus/applicationservice.h index 208679b1..4320de84 100644 --- a/src/dbus/applicationservice.h +++ b/src/dbus/applicationservice.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: LGPL-3.0-or-later @@ -143,9 +143,7 @@ class ApplicationService : public QObject, protected QDBusContext EntryValueType type, const QLocale &locale = getUserLocale()) const noexcept; - [[nodiscard]] LaunchTask unescapeExec(const QString &str, QStringList fields) noexcept; - [[nodiscard]] static std::optional unescapeExecArgs(const QString &str) noexcept; - void unescapeEens(QVariantMap &options) noexcept; + [[nodiscard]] static std::optional splitExecArguments(QStringView str) noexcept; public Q_SLOTS: // NOTE: 'realExec' only for internal implementation @@ -205,6 +203,7 @@ public Q_SLOTS: void setAutostartSource(AutostartSource &&source) noexcept; void appendExtraEnvironments(QVariantMap &runtimeOptions) const noexcept; void processCompatibility(const QString &action, QVariantMap &options, QString &execStr); + [[nodiscard]] LaunchTask processExec(const QString &str, const QStringList& fields, const QString& workingDir) noexcept; [[nodiscard]] ApplicationManager1Service *parent() { return dynamic_cast(QObject::parent()); } [[nodiscard]] const ApplicationManager1Service *parent() const { diff --git a/src/dbus/jobmanager1service.h b/src/dbus/jobmanager1service.h index ee24e972..7b7b0c0c 100644 --- a/src/dbus/jobmanager1service.h +++ b/src/dbus/jobmanager1service.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: LGPL-3.0-or-later @@ -28,14 +28,14 @@ struct LaunchTask LaunchTask(LaunchTask &&) = default; LaunchTask &operator=(const LaunchTask &) = default; LaunchTask &operator=(LaunchTask &&) = default; - explicit operator bool() const { return !LaunchBin.isEmpty() and !command.isEmpty(); } + explicit operator bool() const { return !LaunchBin.isEmpty() && !command.isEmpty(); } QString LaunchBin; QStringList command; QVariantList Resources; bool local{false}; - int argNum{-1}; - int fieldLocation{-1}; + qsizetype argNum{-1}; + qsizetype fieldLocation{-1}; }; Q_DECLARE_METATYPE(LaunchTask) @@ -53,16 +53,16 @@ class JobManager1Service final : public QObject template QDBusObjectPath addJob(const QString &source, F func, QVariantList args) { - static_assert(std::is_invocable_v, "param type must be QVariant."); + static_assert(std::is_invocable_v, "param type must be satisfied with const QVariant&."); - QString objectPath = + const auto objectPath = QString{"%1/%2"}.arg(DDEApplicationManager1JobManager1ObjectPath).arg(QUuid::createUuid().toString(QUuid::Id128)); QFuture future = QtConcurrent::mappedReduced(std::move(args), func, qOverload(&QVariantList::append), QVariantList{}, QtConcurrent::ReduceOption::OrderedReduce); - QSharedPointer job{new (std::nothrow) JobService{future}}; + const QSharedPointer job{new (std::nothrow) JobService{future}}; if (job == nullptr) { qCritical() << "couldn't new JobService."; future.cancel(); @@ -71,7 +71,7 @@ class JobManager1Service final : public QObject auto *ptr = job.data(); auto *adaptor = new (std::nothrow) JobAdaptor(ptr); - if (adaptor == nullptr or !registerObjectToDBus(ptr, objectPath, JobInterface)) { + if (adaptor == nullptr || !registerObjectToDBus(ptr, objectPath, JobInterface)) { qCritical() << "can't register job to dbus."; future.cancel(); return {}; @@ -79,7 +79,7 @@ class JobManager1Service final : public QObject auto path = QDBusObjectPath{objectPath}; { - QMutexLocker locker{&m_mutex}; + const QMutexLocker locker{&m_mutex}; m_jobs.insert(path, job); // Insertion is always successful } emit JobNew(path, QDBusObjectPath{source}); diff --git a/src/desktopentry.cpp b/src/desktopentry.cpp index cb6b293e..ebe2b4b0 100644 --- a/src/desktopentry.cpp +++ b/src/desktopentry.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: LGPL-3.0-or-later @@ -259,63 +259,32 @@ void DesktopEntry::insert(const QString &key, const QString &valueKey, Value &&v outer->insert(valueKey, val); } -QString unescape(const QString &str, bool shellMode) noexcept +QString unescapeValue(QStringView str) noexcept { - QString unescapedStr; - for (qsizetype i = 0; i < str.size(); ++i) { - auto c = str.at(i); - if (c != '\\') { - unescapedStr.append(c); - continue; - } - - switch (str.at(i + 1).toLatin1()) { - default: - unescapedStr.append(c); - break; - case 'n': - unescapedStr.append('\n'); - ++i; - break; - case 't': - unescapedStr.append('\t'); - ++i; - break; - case 'r': - unescapedStr.append('\r'); - ++i; - break; - case '\\': - unescapedStr.append('\\'); - ++i; - break; - case ';': - unescapedStr.append(';'); - ++i; - break; - case 's': { - if (shellMode) { // for wordexp - unescapedStr.append('\\'); + QString out; + out.reserve(str.size()); + + for (const auto *it = str.begin(); it != str.end(); ++it) { + if (*it == u'\\' && (it + 1) != str.end()) { + const auto next = (*(++it)).unicode(); + switch (next) { + case 's': out.append(u' '); break; + case 'n': out.append(u'\n'); break; + case 't': out.append(u'\t'); break; + case 'r': out.append(u'\r'); break; + case '\\': out.append(u'\\'); break; + case ';': out.append(u';'); break; + default: + out.append(u'\\'); + out.append(next); + break; } - unescapedStr.append(' '); - ++i; - } break; + } else { + out.append(*it); } } - return unescapedStr; -} - -QString escapeForWordexp(const QString &str) noexcept -{ - QString escapedStr; - for (const QChar &c : str) { - if (c == '$') { - escapedStr.append('\\'); - } - escapedStr.append(c); - } - return escapedStr; + return out; } QString toString(const DesktopEntry::Value &value) noexcept @@ -333,7 +302,7 @@ QString toString(const DesktopEntry::Value &value) noexcept return {}; } - auto unescapedStr = unescape(str); + auto unescapedStr = unescapeValue(str); if (hasNonAsciiAndControlCharacters(unescapedStr)) { return {}; @@ -347,7 +316,7 @@ QString toLocaleString(const QStringMap &localeMap, const QLocale &locale) noexc for (auto it = localeMap.constKeyValueBegin(); it != localeMap.constKeyValueEnd(); ++it) { auto [a, b] = *it; if (QLocale{a}.name() == locale.name()) { - return unescape(b); + return unescapeValue(b); } } diff --git a/src/desktopentry.h b/src/desktopentry.h index 8cb5f490..b0b65d3a 100644 --- a/src/desktopentry.h +++ b/src/desktopentry.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: LGPL-3.0-or-later @@ -149,8 +149,7 @@ bool operator==(const DesktopFile &lhs, const DesktopFile &rhs); bool operator!=(const DesktopFile &lhs, const DesktopFile &rhs); -QString unescape(const QString &str, bool shellMode = false) noexcept; -QString escapeForWordexp(const QString &str) noexcept; +QString unescapeValue(QStringView str) noexcept; QString toLocaleString(const QStringMap &localeMap, const QLocale &locale) noexcept; diff --git a/src/global.h b/src/global.h index bde95d9a..6f40c7f7 100644 --- a/src/global.h +++ b/src/global.h @@ -1,32 +1,32 @@ -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: LGPL-3.0-or-later #ifndef GLOBAL_H #define GLOBAL_H -#include -#include +#include "config.h" +#include "constant.h" +#include #include -#include #include -#include -#include -#include -#include -#include +#include #include #include -#include -#include -#include -#include +#include +#include #include +#include +#include +#include +#include +#include +#include +#include // IWYU pragma: keep +#include #include #include -#include -#include "constant.h" -#include "config.h" +#include Q_DECLARE_LOGGING_CATEGORY(DDEAMProf) @@ -149,7 +149,7 @@ inline const QDBusArgument &operator>>(const QDBusArgument &argument, QList -#include #include "global.h" -#include +#include +#include #include +#include namespace { void registerComplexDbusType() // FIXME: test shouldn't associate with DBus diff --git a/tests/ut_escape.cpp b/tests/ut_escape.cpp new file mode 100644 index 00000000..214ecd19 --- /dev/null +++ b/tests/ut_escape.cpp @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "desktopentry.h" +#include "dbus/applicationservice.h" +#include +#include +#include + +TEST(ApplicationServiceTest, UnescapeValue_Standard) +{ + struct TestCase + { + QString input; + QString expected; + QString reason; + }; + + const QList testCases = {{R"(Space\sTest)", "Space Test", "Support for ASCII space"}, + {R"(Line1\nLine2)", "Line1\nLine2", "Support for newline"}, + {R"(Tab\tTest)", "Tab\tTest", "Support for tab"}, + {R"(Return\rTest)", "Return\rTest", "Support for carriage return"}, + {R"(Backslash\\Test)", R"(Backslash\Test)", "Support for backslash"}, + {R"(Value1\;Value2)", "Value1;Value2", "Support for semicolons"}, + {R"(\\s)", R"(\s)", R"(Double backslash before 's' should result in literal '\s')"}, + {R"(\s\n\t\r\\)", " \n\t\r\\", "Multiple escapes in sequence"}, + {R"(\)", R"(\)", "Trailing backslash at the end of string should be preserved as literal"}, + {R"(\")", R"(\")", "Double quote should NOT be unescaped in first pass"}, + {R"(\$)", R"(\$)", "Dollar sign should NOT be unescaped in first pass"}, + {R"(\b)", R"(\b)", "Unknown escape sequences should preserve the backslash"}, + {"/path/to/bi_na=ry", "/path/to/bi_na=ry", "Escaped characters should be preserved"}}; + + for (const auto &tc : testCases) { + const auto result = unescapeValue(tc.input); + EXPECT_EQ(result, tc.expected) << "Failed: " << tc.reason.toStdString() << "\nInput: " << tc.input.toStdString(); + } +} + +TEST(ApplicationServiceTest, SplitExecArguments_PassPhase2_Spec) +{ + struct TestCase + { + QString input; // after first pass + std::optional expected; + QString reason; + }; + + const QList testCases = { + {R"(myapp arg1 %f)", QStringList{"myapp", "arg1", "%f"}, "Simple split and placeholder preservation"}, + {R"(myapp "quoted arg" next)", QStringList{"myapp", "quoted arg", "next"}, "Basic double quotes handling"}, + {R"(myapp "with \"internal\" quotes")", + QStringList{"myapp", R"(with "internal" quotes)"}, + "Escaped quotes inside a quoted string"}, + {R"(myapp /path/with\ space)", QStringList{"myapp", "/path/with space"}, "Unquoted: backslash escapes a space"}, + {R"(myapp path\\with\\backslash)", + QStringList{"myapp", R"(path\with\backslash)"}, + "Unquoted: double backslash becomes single literal backslash"}, + {R"(myapp "cost \$100")", QStringList{"myapp", "cost $100"}, "Quoted: dollar sign is a special char, backslash removed"}, + {R"(myapp "a\b")", QStringList{"myapp", R"(a\b)"}, "Quoted: 'b' is not special, so backslash is PRESERVED"}, + {R"(myapp "path\\with\\backslash")", + QStringList{"myapp", R"(path\with\backslash)"}, + R"(Quoted: Passphase 2 reduces \\ to \ )"}, + {R"(myapp --icon=%i --file %f)", + QStringList{"myapp", "--icon=%i", "--file", "%f"}, + "Field codes can be part of an argument"}, + {R"(myapp "unclosed quote)", std::nullopt, "Unclosed quote should return nullopt"}, + {R"(myapp \)", std::nullopt, "Trailing backslash is illegal"}}; + + for (const auto &tc : testCases) { + auto result = ApplicationService::splitExecArguments(tc.input); + EXPECT_EQ(result, tc.expected) << "Input: " << tc.input.toStdString() + << "\nExpected: " << tc.expected.value_or(QStringList{}).join('|').toStdString() + << "\nActual: " << result.value_or(QStringList{}).join("|").toStdString() + << "\nReason: " << tc.reason.toStdString(); + } +} diff --git a/tests/ut_escapeexec.cpp b/tests/ut_escapeexec.cpp deleted file mode 100644 index 5b0adc77..00000000 --- a/tests/ut_escapeexec.cpp +++ /dev/null @@ -1,91 +0,0 @@ -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. -// -// SPDX-License-Identifier: LGPL-3.0-or-later - -#include "dbus/applicationservice.h" -#include -#include - -TEST(UnescapeExec, blankSpace) -{ - QList>> testCases{ - { - R"(/usr/bin/hello\sworld --arg1=val1 -h --str="rrr ggg bbb")", - { - "/usr/bin/hello world", - "--arg1=val1", - "-h", - "--str=rrr ggg bbb", - }, - }, - { - R"("/usr/bin/hello world" a b -- "c d")", - { - "/usr/bin/hello world", - "a", - "b", - "--", - "c d", - }, - }, - { - R"("/usr/bin/hello\t\nworld" a b -- c d)", - { - "/usr/bin/hello\t\nworld", - "a", - "b", - "--", - "c", - "d", - }, - }, - }; - - for (auto &testCase : testCases) { - EXPECT_EQ(ApplicationService::unescapeExecArgs(testCase.first), testCase.second); - } -} - -TEST(UnescapeExec, dollarSignEscape) -{ - // Test that $$ in file paths is preserved and not interpreted as process ID - QList>> testCases{ - { - R"(/path/to/file$$name.txt)", - { - "/path/to/file$$name.txt", - }, - }, - { - R"(/home/user/document$$2023.pdf)", - { - "/home/user/document$$2023.pdf", - }, - }, - { - R"($$double-dollar-test)", - { - "$$double-dollar-test", - }, - }, - { - R"(/mixed/path$$with/normal/parts.txt)", - { - "/mixed/path$$with/normal/parts.txt", - }, - }, - { - R"(/usr/bin/app --file=/path/with$$dollars.txt --other=normal)", - { - "/usr/bin/app", - "--file=/path/with$$dollars.txt", - "--other=normal", - }, - }, - }; - - for (auto &testCase : testCases) { - auto result = ApplicationService::unescapeExecArgs(testCase.first); - EXPECT_EQ(result, testCase.second) << "Failed for input: " << testCase.first.toStdString(); - } -} diff --git a/tests/ut_jobmanager.cpp b/tests/ut_jobmanager.cpp index 54805f5a..d39ef8d2 100644 --- a/tests/ut_jobmanager.cpp +++ b/tests/ut_jobmanager.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: LGPL-3.0-or-later @@ -38,10 +38,10 @@ TEST_F(TestJobManager, addJob) manager.addJob( sourcePath.path(), - [](auto value) -> QVariant { + [](const auto &value) -> QVariant { EXPECT_TRUE(value.toString() == "Application"); return QVariant::fromValue(true); }, - args); + std::move(args)); QThreadPool::globalInstance()->waitForDone(); }