From 1c26ebd111eb6c7be6caa10386ad85103b7e928e Mon Sep 17 00:00:00 2001 From: ComixHe Date: Mon, 27 Apr 2026 15:31:33 +0800 Subject: [PATCH 1/2] feat(util): support full Unicode with UTF-8 byte escaping Desktop name and object path specs require ASCII-only. Now escapes all UTF-8 bytes to support Chinese, Japanese, Korean, etc. Bug: PMS-357251 Signed-off-by: ComixHe --- include/util/dutil.h | 61 +++++++++++++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/include/util/dutil.h b/include/util/dutil.h index 4353b854..c41e5d3b 100644 --- a/include/util/dutil.h +++ b/include/util/dutil.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2016 - 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2016 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: LGPL-3.0-or-later @@ -54,37 +54,58 @@ void SecureErase(T &obj) } } -inline QString escapeToObjectPath(const QString &str) +inline QString escapeToObjectPath(const QByteArray &str) noexcept { if (str.isEmpty()) { - return "_"; + return QStringLiteral("_"); } - auto ret = str; - QRegularExpression re{R"([^a-zA-Z0-9])"}; - auto matcher = re.globalMatch(ret); - while (matcher.hasNext()) { - auto replaceList = matcher.next().capturedTexts(); - replaceList.removeDuplicates(); - for (const auto &c : replaceList) { - auto hexStr = QString::number(static_cast(c.front().toLatin1()), 16); - ret.replace(c, QString{R"(_%1)"}.arg(hexStr)); + QString ret; + ret.reserve(str.size() * 3); + + for (qsizetype i = 0; i < str.size(); ++i) { + auto byte = static_cast(str.at(i)); + if (std::isalnum(byte) != 0 || byte == '/') { + ret.append(QChar::fromLatin1(byte)); + } else { + // TODO: a valid dbus object path component only allows "[A-Z][a-z][0-9]_" + // for compatibility with existing applications, we escape all unicode to avoid breakage + // but we should consider to drop this compatibility hack in the future. + ret.append(u'_'); + ret.append(QString::number(byte, 16).rightJustified(2, u'0').toLower()); } } + + ret.shrink_to_fit(); return ret; } -inline QString unescapeFromObjectPath(const QString &str) +inline QString escapeToObjectPath(const QString &str) noexcept +{ + return escapeToObjectPath(str.toUtf8()); +} + +inline QString unescapeFromObjectPath(const QString &str) noexcept { - auto ret = str; - for (int i = 0; i < str.size(); ++i) { - if (str[i] == '_' && i + 2 < str.size()) { - auto hexStr = str.mid(i + 1, 2); - ret.replace(QString{"_%1"}.arg(hexStr), QChar::fromLatin1(hexStr.toUInt(nullptr, 16))); - i += 2; + QByteArray ret; + ret.reserve(str.length()); + + for (qsizetype i = 0; i < str.length();) { + if (i <= str.length() - 3 && str.at(i) == u'_') { + bool ok{false}; + auto byte = static_cast(str.mid(i + 1, 2).toUShort(&ok, 16)); + if (ok) { + ret.append(static_cast(byte)); + i += 3; + continue; + } } + + ret.append(str.at(i).toLatin1()); + ++i; } - return ret; + + return QString::fromUtf8(ret); } inline QString getAppIdFromAbsolutePath(const QString &path) From 7f2d47416f83457bd06c22d1329961514a61c52a Mon Sep 17 00:00:00 2001 From: ComixHe Date: Mon, 27 Apr 2026 15:59:59 +0800 Subject: [PATCH 2/2] refactor(util): move inline function to library codebase Signed-off-by: ComixHe --- include/util/dutil.h | 96 +++------------------------------------ src/util/dutil.cpp | 105 +++++++++++++++++++++++++++++++++++++++++++ src/util/util.cmake | 2 + 3 files changed, 112 insertions(+), 91 deletions(-) create mode 100644 src/util/dutil.cpp diff --git a/include/util/dutil.h b/include/util/dutil.h index c41e5d3b..7340dff4 100644 --- a/include/util/dutil.h +++ b/include/util/dutil.h @@ -54,99 +54,13 @@ void SecureErase(T &obj) } } -inline QString escapeToObjectPath(const QByteArray &str) noexcept -{ - if (str.isEmpty()) { - return QStringLiteral("_"); - } - - QString ret; - ret.reserve(str.size() * 3); - - for (qsizetype i = 0; i < str.size(); ++i) { - auto byte = static_cast(str.at(i)); - if (std::isalnum(byte) != 0 || byte == '/') { - ret.append(QChar::fromLatin1(byte)); - } else { - // TODO: a valid dbus object path component only allows "[A-Z][a-z][0-9]_" - // for compatibility with existing applications, we escape all unicode to avoid breakage - // but we should consider to drop this compatibility hack in the future. - ret.append(u'_'); - ret.append(QString::number(byte, 16).rightJustified(2, u'0').toLower()); - } - } - - ret.shrink_to_fit(); - return ret; -} +QString escapeToObjectPath(const QByteArray &str) noexcept; -inline QString escapeToObjectPath(const QString &str) noexcept -{ - return escapeToObjectPath(str.toUtf8()); -} - -inline QString unescapeFromObjectPath(const QString &str) noexcept -{ - QByteArray ret; - ret.reserve(str.length()); +QString escapeToObjectPath(const QString &str) noexcept; - for (qsizetype i = 0; i < str.length();) { - if (i <= str.length() - 3 && str.at(i) == u'_') { - bool ok{false}; - auto byte = static_cast(str.mid(i + 1, 2).toUShort(&ok, 16)); - if (ok) { - ret.append(static_cast(byte)); - i += 3; - continue; - } - } +QString unescapeFromObjectPath(const QString &str) noexcept; - ret.append(str.at(i).toLatin1()); - ++i; - } - - return QString::fromUtf8(ret); -} +QString getAppIdFromAbsolutePath(const QString &path) noexcept; -inline QString getAppIdFromAbsolutePath(const QString &path) -{ - static QString desktopSuffix{u8".desktop"}; - const auto &appDirs = QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation); - if (!path.endsWith(desktopSuffix) || - !std::any_of(appDirs.cbegin(), appDirs.constEnd(), [&path](const QString &dir) { return path.startsWith(dir); })) { - return {}; - } - - auto tmp = path.chopped(desktopSuffix.size()); - auto components = tmp.split(QDir::separator(), Qt::SkipEmptyParts); - auto location = std::find(components.cbegin(), components.cend(), "applications"); - if (location == components.cend()) { - return {}; - } - - auto appId = QStringList{location + 1, components.cend()}.join('-'); - return appId; -} - -inline QStringList getAbsolutePathFromAppId(const QString &appId) -{ - auto components = appId.split('-', Qt::SkipEmptyParts); - auto appDirs = QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation); - - QStringList ret; - for (const auto &dirPath : appDirs) { - auto currentDir = dirPath; - for (auto it = components.cbegin(); it != components.cend(); ++it) { - auto currentName = QStringList{it, components.cend()}.join('-') + QString{".desktop"}; - QDir dir{currentDir}; - if (dir.exists(currentName)) { - ret.append(dir.filePath(currentName)); - } - - currentDir.append(QDir::separator() + *it); - } - } - - return ret; -} +QStringList getAbsolutePathFromAppId(const QString &appId) noexcept; } diff --git a/src/util/dutil.cpp b/src/util/dutil.cpp new file mode 100644 index 00000000..09a5769d --- /dev/null +++ b/src/util/dutil.cpp @@ -0,0 +1,105 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "dutil.h" + +namespace DUtil { + +QString escapeToObjectPath(const QByteArray &str) noexcept +{ + if (str.isEmpty()) { + return QStringLiteral("_"); + } + + QString ret; + ret.reserve(str.size() * 3); + + for (qsizetype i = 0; i < str.size(); ++i) { + auto byte = static_cast(str.at(i)); + if (std::isalnum(byte) != 0 || byte == '/') { + ret.append(QChar::fromLatin1(byte)); + } else { + // TODO: a valid dbus object path component only allows "[A-Z][a-z][0-9]_" + // for compatibility with existing applications, we escape all unicode to avoid breakage + // but we should consider to drop this compatibility hack in the future. + ret.append(u'_'); + ret.append(QString::number(byte, 16).rightJustified(2, u'0').toLower()); + } + } + + ret.shrink_to_fit(); + return ret; +} + +QString escapeToObjectPath(const QString &str) noexcept +{ + return escapeToObjectPath(str.toUtf8()); +} + +QString unescapeFromObjectPath(const QString &str) noexcept +{ + QByteArray ret; + ret.reserve(str.length()); + + for (qsizetype i = 0; i < str.length();) { + if (i <= str.length() - 3 && str.at(i) == u'_') { + bool ok{false}; + auto byte = static_cast(str.mid(i + 1, 2).toUShort(&ok, 16)); + if (ok) { + ret.append(static_cast(byte)); + i += 3; + continue; + } + } + + ret.append(str.at(i).toLatin1()); + ++i; + } + + return QString::fromUtf8(ret); +} + +QString getAppIdFromAbsolutePath(const QString &path) noexcept +{ + static QString desktopSuffix{u8".desktop"}; + const auto &appDirs = QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation); + if (!path.endsWith(desktopSuffix) || + !std::any_of(appDirs.cbegin(), appDirs.constEnd(), [&path](const QString &dir) { return path.startsWith(dir); })) { + return {}; + } + + auto tmp = path.chopped(desktopSuffix.size()); + auto components = tmp.split(QDir::separator(), Qt::SkipEmptyParts); + auto location = std::find(components.cbegin(), components.cend(), "applications"); + if (location == components.cend()) { + return {}; + } + + auto appId = QStringList{location + 1, components.cend()}.join('-'); + return appId; +} + +QStringList getAbsolutePathFromAppId(const QString &appId) noexcept +{ + auto components = appId.split('-', Qt::SkipEmptyParts); + auto appDirs = QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation); + + QStringList ret; + for (const auto &dirPath : appDirs) { + auto currentDir = dirPath; + for (auto it = components.cbegin(); it != components.cend(); ++it) { + auto currentName = QStringList{it, components.cend()}.join('-') + QString{".desktop"}; + QDir dir{currentDir}; + if (dir.exists(currentName)) { + ret.append(dir.filePath(currentName)); + } + + currentDir.append(QDir::separator() + *it); + } + } + + return ret; +} + +} diff --git a/src/util/util.cmake b/src/util/util.cmake index cda8f50b..ce351977 100644 --- a/src/util/util.cmake +++ b/src/util/util.cmake @@ -17,6 +17,7 @@ if(LINUX) ${CMAKE_CURRENT_LIST_DIR}/ddbusextendedabstractinterface.cpp ${CMAKE_CURRENT_LIST_DIR}/ddbusextendedpendingcallwatcher.cpp ${CMAKE_CURRENT_LIST_DIR}/dtextencoding.cpp + ${CMAKE_CURRENT_LIST_DIR}/dutil.cpp ) else() set(UTILS_SOURCES @@ -37,6 +38,7 @@ else() ${CMAKE_CURRENT_LIST_DIR}/ddbusextendedabstractinterface.cpp ${CMAKE_CURRENT_LIST_DIR}/ddbusextendedpendingcallwatcher.cpp ${CMAKE_CURRENT_LIST_DIR}/dtextencoding.cpp + ${CMAKE_CURRENT_LIST_DIR}/dutil.cpp ) endif() file(GLOB UTILS_HEADERS