Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/app-identifier/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ add_executable(${BIN_NAME} main.cpp)
target_link_libraries(${BIN_NAME} PRIVATE
Qt6::Core
Qt6::DBus
Dtk6::Core
)

target_include_directories(${BIN_NAME} PRIVATE
Expand Down
2 changes: 1 addition & 1 deletion apps/dde-am/src/launcher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ QString Launcher::appId() const noexcept
const auto endIndex = pathView.indexOf(u'/', startIndex + 1);
const auto id =
endIndex <= -1 ? pathView.sliced(startIndex + 1) : pathView.sliced(startIndex + 1, endIndex - (startIndex + 1));
return unescapeFromObjectPath(id);
return DUtil::unescapeFromObjectPath(id.toString());
}

void Launcher::setAutostart(bool autostart)
Expand Down
4 changes: 2 additions & 2 deletions apps/dde-am/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,10 @@ int handleLaunchApp(const QCommandLineParser &parser,
QString appPath;
if (!appId.isEmpty()) {
// 解析出 appId,说明输入是 appId 或 desktop 文件
appPath = fromStaticRaw(DDEApplicationManager1ObjectPath) % u'/' % escapeToObjectPath(appId);
appPath = fromStaticRaw(DDEApplicationManager1ObjectPath) % u'/' % DUtil::escapeToObjectPath(appId);
} else if (!inputArg.isEmpty() && !inputArg.startsWith(u'/')) {
// 既不是绝对路径,也不是 desktop 文件等,视为 appId
appPath = fromStaticRaw(DDEApplicationManager1ObjectPath) % u'/' % escapeToObjectPath(inputArg);
appPath = fromStaticRaw(DDEApplicationManager1ObjectPath) % u'/' % DUtil::escapeToObjectPath(inputArg);
} else {
// 绝对路径或特殊用法
qWarning() << "unknown input: " << inputArg << ", just use it as path.";
Expand Down
2 changes: 1 addition & 1 deletion debian/control
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Build-Depends: debhelper-compat ( =12), cmake,
pkg-config,
qt6-base-dev,
libdtkcommon-dev,
libdtk6core-dev,
libdtk6core-dev (>= 6.7.41),
treeland-protocols,
qt6-wayland-dev,
qt6-base-private-dev,
Expand Down
145 changes: 54 additions & 91 deletions src/global.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include <sys/syscall.h>
#include <unistd.h>
#include <QDBusAbstractAdaptor>
#include <DUtil>

Q_DECLARE_LOGGING_CATEGORY(DDEAMProf)
Q_DECLARE_LOGGING_CATEGORY(DDEAMUtils)
Expand Down Expand Up @@ -422,136 +423,98 @@ inline QLocale getUserLocale()

namespace Detail {

inline bool isBaseAlnum(QChar ch) noexcept
inline QByteArray unescapeToBytes(const QStringView str, const QStringView prefix) noexcept
{
const ushort cell = ch.unicode();
return (cell >= u'a' && cell <= u'z') || (cell >= u'A' && cell <= u'Z') || (cell >= u'0' && cell <= u'9');
}

inline QString unescapeImpl(QStringView str, QStringView prefix) noexcept
{
const auto prefixLen = prefix.size();
const auto step = prefixLen + 2;
const auto len = str.size();
const auto len = str.length();
const auto prefixLen = prefix.length();

auto r = str.indexOf(prefix);
if (r == -1 || r > len - step) {
return str.toString();
}

QString result;
QByteArray result;
result.reserve(len);
result.append(str.first(r));

while (r < len) {
if (r <= len - step && str.sliced(r, prefixLen) == prefix) {
for (qsizetype i = 0; i < len;) {
if (i <= len - prefixLen - 2 && str.mid(i, prefixLen) == prefix) {
bool ok{false};
const auto val = str.sliced(r + prefixLen, 2).toShort(&ok, 16);

auto byte = static_cast<unsigned char>(str.mid(i + prefixLen, 2).toUShort(&ok, 16));
if (ok) {
result.append(QChar::fromLatin1(static_cast<char>(val)));
r += step;
result.append(static_cast<char>(byte));
i += prefixLen + 2;
continue;
}
}

result.append(str.at(r++));
}

if (r < len) {
result.append(str.sliced(r));
result.append(str.at(i).toLatin1());
++i;
}

result.shrink_to_fit();
return result;
}

} // namespace Detail

inline QString escapeToObjectPath(QStringView str)
inline QString escapeApplicationId(QByteArrayView utf8)
{
using namespace Qt::StringLiterals;
if (str.isEmpty()) {
return u"_"_s;
}

// see: https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-marshaling-object-path
auto isSafe = [](QChar ch) { return Detail::isBaseAlnum(ch) || ch == u'_' || ch == u'/'; };

const auto *it = std::find_if_not(str.cbegin(), str.cend(), isSafe);
if (it == str.cend()) {
return str.toString();
}

QString result;
result.reserve(str.size() + 16);
result.append(str.first(std::distance(str.cbegin(), it)));

for (; it != str.end(); ++it) {
const auto ch = *it;
if (isSafe(ch)) {
result.append(ch);
} else {
if (ch.row() != 0) {
qCWarning(DDEAMUtils).nospace()
<< "Character U+" << Qt::hex << ch.unicode() << " is truncated to " << static_cast<uint>(ch.cell());
}

result.append(u'_');
result.append(QString::number(ch.cell(), 16).rightJustified(2, u'0').toLower());
}
}

return result;
}

inline QString unescapeFromObjectPath(QStringView str)
{
using namespace Qt::StringLiterals;
return Detail::unescapeImpl(str, u"_"_s);
}

inline QString escapeApplicationId(QStringView id)
{
if (id.isEmpty()) {
return id.toString();
if (utf8.isEmpty()) {
return {};
}

using namespace Qt::StringLiterals;

// see:
// https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html#String%20Escaping%20for%20Inclusion%20in%20Unit%20Names
auto isSafeChar = [](QChar ch) { return Detail::isBaseAlnum(ch) || ch == u'_' || ch == u':' || ch == u'.'; };
//
// This function escapes UTF-8 byte sequences to valid systemd unit names.
// Each non-safe byte is escaped as "\xxx" (2-digit lowercase hex).
// Safe characters are: [A-Z][a-z][0-9]:_.
// '/' is replaced by '-' (special handling for path-style IDs)
// '.' at the start is escaped; other '.' are safe

// Custom escape that handles "/" specially
QString result;
result.reserve(id.size() + 16);
result.reserve(utf8.size() * 4); // Worst case: every byte needs "\xxx" (4 chars)

bool hasNonAscii{false};

for (qsizetype i = 0; i < id.size(); ++i) {
const auto ch = id.at(i);
for (qsizetype i = 0; i < utf8.size(); ++i) {
auto byte = static_cast<unsigned char>(utf8.at(i));

if (ch == u'/') {
// "/" is replaced by "-"
if (byte == '/') {
result.append(u'-');
} else if (i == 0 && ch == u'.') {
// "." as first char is escaped
} else if (i == 0 && byte == '.') {
result.append(uR"(\x)"_s);
result.append(QString::number(ch.cell(), 16).rightJustified(2, u'0').toLower());
} else if (isSafeChar(ch)) {
result.append(ch);
result.append(QString::number(byte, 16).rightJustified(2, u'0').toLower());
} else if (std::isalnum(byte) || byte == ':' || byte == '_' || byte == '.') {
result.append(QChar::fromLatin1(byte));
} else {
// Other non-safe chars are escaped
if (byte > 0x7F) {
hasNonAscii = true;
}

result.append(uR"(\x)"_s);
result.append(QString::number(ch.cell(), 16).rightJustified(2, u'0').toLower());
result.append(QString::number(byte, 16).rightJustified(2, u'0').toLower());
}
}

if (hasNonAscii) {
qCWarning(DDEAMUtils) << "String contains non-ASCII characters, which "
"does not conform to the systemd specification. A "
"compatible ID is generated using UTF-8 byte escaping for now,"
"but this compatibility may be removed in the future."
"Please use ASCII-only names.";
}

result.shrink_to_fit();
return result;
}

inline QString escapeApplicationId(QStringView str)
{
return escapeApplicationId(str.toUtf8());
}

inline QString unescapeApplicationId(QStringView id)
{
using namespace Qt::StringLiterals;
return Detail::unescapeImpl(id, uR"(\x)"_s);
return QString::fromUtf8(Detail::unescapeToBytes(id, uR"(\x)"_s));
}

inline QString getRelativePathFromAppId(QStringView id)
Expand Down Expand Up @@ -788,7 +751,7 @@ inline QByteArray getCurrentSessionId()

auto msg = QDBusMessage::createMethodCall(QString::fromUtf8(SystemdService),
QString::fromUtf8(SystemdObjectPath) % u"/unit/"_s %
escapeToObjectPath(u"graphical-session.target"),
DUtil::escapeToObjectPath(u"graphical-session.target"_s),
fromStaticRaw(SystemdPropInterfaceName),
u"Get"_s);
msg << fromStaticRaw(SystemdUnitInterfaceName);
Expand Down Expand Up @@ -880,7 +843,7 @@ inline QString getObjectPathFromAppId(const QString &appId)
{
const auto &basePath = fromStaticRaw(DDEApplicationManager1ObjectPath);
if (!appId.isEmpty()) {
return basePath % '/' % escapeToObjectPath(appId);
return basePath % '/' % DUtil::escapeToObjectPath(appId);
}

return basePath % '/' % QUuid::createUuid().toString(QUuid::Id128);
Expand Down
Loading
Loading