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
12 changes: 12 additions & 0 deletions api/dbus/org.desktopspec.ApplicationManager1.Application.xml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,18 @@
<annotation name="org.qtproject.QtDBus.QtTypeName" value="QStringMap"/>
</property>

<property name="Keywords" type="a{sas}" access="read">
<annotation
name="org.freedesktop.DBus.Description"
value="Keywords of this application, keyed by locale. Each value is
the keyword list split and unescaped according to the desktop
entry specification. The unlocalized value is stored under the
key 'default'. Consumers must apply the standard locale fallback
rules; the locale dimension is intentionally preserved."
/>
<annotation name="org.qtproject.QtDBus.QtTypeName" value="QStringListMap"/>
</property>

<property name="StartupWMClass" type="s" access="read">
<annotation
name="org.freedesktop.DBus.Description"
Expand Down
2 changes: 2 additions & 0 deletions apps/dde-am/src/commandexecutor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ void registerComplexDbusType()
qDBusRegisterMetaType<ObjectMap>();
qDBusRegisterMetaType<QStringMap>();
qRegisterMetaType<QStringMap>();
qRegisterMetaType<QStringListMap>();
qDBusRegisterMetaType<QStringListMap>();
qRegisterMetaType<PropMap>();
qDBusRegisterMetaType<PropMap>();
qDBusRegisterMetaType<QDBusObjectPath>();
Expand Down
2 changes: 2 additions & 0 deletions apps/dde-am/src/launcher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ void registerComplexDbusType()
qDBusRegisterMetaType<ObjectMap>();
qDBusRegisterMetaType<QStringMap>();
qRegisterMetaType<QStringMap>();
qRegisterMetaType<QStringListMap>();
qDBusRegisterMetaType<QStringListMap>();
qRegisterMetaType<PropMap>();
qDBusRegisterMetaType<PropMap>();
qDBusRegisterMetaType<QDBusObjectPath>();
Expand Down
2 changes: 2 additions & 0 deletions apps/dde-application-manager/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ void registerComplexDbusType()
qDBusRegisterMetaType<ObjectMap>();
qDBusRegisterMetaType<QStringMap>();
qRegisterMetaType<QStringMap>();
qRegisterMetaType<QStringListMap>();
qDBusRegisterMetaType<QStringListMap>();
qRegisterMetaType<PropMap>();
qDBusRegisterMetaType<PropMap>();
qDBusRegisterMetaType<QDBusObjectPath>();
Expand Down
11 changes: 11 additions & 0 deletions src/dbus/applicationservice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -794,6 +794,16 @@ QStringMap ApplicationService::genericName() const noexcept
return value->get().value<QStringMap>();
}

QStringListMap ApplicationService::keywords() const noexcept
{
auto value = m_entry->value(fromStaticRaw(DesktopFileEntryKey), fromStaticRaw(DesktopEntryKeywords));
if (!value) {
return {};
}

return toKeywordMap(value->get());
}

QStringMap ApplicationService::icons() const noexcept
{
QStringMap ret;
Expand Down Expand Up @@ -1457,6 +1467,7 @@ void ApplicationService::resetEntry(DesktopEntry *newEntry) noexcept
emit iconsChanged();
emit nameChanged();
emit genericNameChanged();
emit keywordsChanged();
emit actionNameChanged();
emit actionsChanged();
emit categoriesChanged();
Expand Down
6 changes: 6 additions & 0 deletions src/dbus/applicationservice.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ class ApplicationService : public QObject, protected QDBusContext
Q_PROPERTY(QStringMap GenericName READ genericName NOTIFY genericNameChanged)
[[nodiscard]] QStringMap genericName() const noexcept;

// Expose parsed Keywords by locale so clients can search them without rereading desktop files.
Q_PROPERTY(QStringListMap Keywords READ keywords NOTIFY keywordsChanged)
[[nodiscard]] QStringListMap keywords() const noexcept;

Q_PROPERTY(QStringMap Icons READ icons NOTIFY iconsChanged)
[[nodiscard]] QStringMap icons() const noexcept;

Expand Down Expand Up @@ -183,6 +187,8 @@ public Q_SLOTS:
void iconsChanged();
void nameChanged();
void genericNameChanged();
// Notify consumers to refresh their search terms when desktop metadata changes.
void keywordsChanged();
void actionNameChanged();
void actionsChanged();
void categoriesChanged();
Expand Down
77 changes: 77 additions & 0 deletions src/desktopentry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,83 @@ QString toLocaleString(const DesktopEntry::Value &localeEntry, const QLocale &lo
return {};
}

namespace {

QStringList parseKeywordList(QStringView rawValue) noexcept
{
QStringList keywords;
QString current;
bool escaped{false};

auto flushKeyword = [&keywords, &current]() {
const auto keyword = current.trimmed();
if (!keyword.isEmpty() && !keywords.contains(keyword, Qt::CaseInsensitive)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Large keyword lists stall manager

When a local user or application can supply a desktop entry with a very large number of distinct Keywords and a D-Bus client reads that application's Keywords property, flushKeyword checks each keyword against all previously collected entries with a case-insensitive linear scan, making parsing quadratic; ApplicationService::keywords() invokes this parser when serving the property, so repeated reads of an entry with a very large list can keep the manager busy and delay other clients.

Use a set for case-insensitive duplicate detection, or impose a reasonable limit on keyword count and total input size.

Prompt for AI agents
In `src/desktopentry.cpp` at line 490:

**Large keyword lists stall manager**

When a local user or application can supply a desktop entry with a very large number of distinct Keywords and a D-Bus client reads that application's Keywords property, `flushKeyword` checks each keyword against all previously collected entries with a case-insensitive linear scan, making parsing quadratic; `ApplicationService::keywords()` invokes this parser when serving the property, so repeated reads of an entry with a very large list can keep the manager busy and delay other clients.

Use a set for case-insensitive duplicate detection, or impose a reasonable limit on keyword count and total input size.

keywords.append(keyword);
}
current.clear();
};

for (const auto ch : rawValue) {
if (escaped) {
switch (ch.unicode()) {
case 's':
current.append(u' ');
break;
case 'n':
current.append(u'\n');
break;
case 't':
current.append(u'\t');
break;
case 'r':
current.append(u'\r');
break;
case '\\':
current.append(u'\\');
break;
case ';':
current.append(u';');
break;
default:
current.append(u'\\');
current.append(ch);
break;
}
escaped = false;
} else if (ch == u'\\') {
escaped = true;
} else if (ch == u';') {
flushKeyword();
} else {
current.append(ch);
}
}

if (escaped) {
current.append(u'\\');
}
flushKeyword();

return keywords;
}

} // namespace

QStringListMap toKeywordMap(const DesktopEntry::Value &value) noexcept
{
QStringListMap keywordMap;
if (value.userType() != QMetaTypeId<QStringMap>::qt_metatype_id()) {
return keywordMap;
}

const auto &localeMap = *static_cast<const QStringMap *>(value.constData());
for (auto it = localeMap.cbegin(); it != localeMap.cend(); ++it) {
keywordMap.insert(it.key(), parseKeywordList(it.value()));
}

return keywordMap;
}

QString toIconString(const DesktopEntry::Value &value) noexcept
{
return toString(value);
Expand Down
6 changes: 6 additions & 0 deletions src/desktopentry.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
#define DESKTOPENTRY_H

#include <QString>
#include <QStringList>
#include <QDebug>
#include <QLocale>
#include <optional>
#include <QFile>
#include <QFileInfo>
#include "iniParser.h"
#include "global.h"

enum class EntryContext : uint8_t { Unknown, EntryOuter, Entry, Done };

Expand Down Expand Up @@ -163,6 +165,10 @@ QString toLocaleString(const DesktopEntry::Value &localeEntry, const QLocale &lo

QString toString(const DesktopEntry::Value &value, bool skipUnescape = false) noexcept;

// Unlike toLocaleString(), decode each locale's Keywords list without selecting a locale.
// Preserve locale keys so consumers can apply their existing fallback policy.
QStringListMap toKeywordMap(const DesktopEntry::Value &value) noexcept;

bool toBoolean(const DesktopEntry::Value &value, bool &ok) noexcept;

QString toIconString(const DesktopEntry::Value &value) noexcept;
Expand Down
5 changes: 5 additions & 0 deletions src/global.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include <QMap>
#include <QMetaClassInfo>
#include <QString>
#include <QStringList>
#include <QUuid>
#include <csignal> // IWYU pragma: keep
#include <optional>
Expand All @@ -34,11 +35,15 @@ Q_DECLARE_LOGGING_CATEGORY(DDEAMUtils)
using ObjectInterfaceMap = QMap<QString, QVariantMap>;
using ObjectMap = QMap<QDBusObjectPath, ObjectInterfaceMap>;
using QStringMap = QMap<QString, QString>;
// Keywords needs a list per locale, unlike QStringMap's single string per locale.
// Keep locale keys (including "default") so clients can choose their fallback.
using QStringListMap = QMap<QString, QStringList>;
using PropMap = QMap<QString, QStringMap>;

Q_DECLARE_METATYPE(ObjectInterfaceMap)
Q_DECLARE_METATYPE(ObjectMap)
Q_DECLARE_METATYPE(QStringMap)
Q_DECLARE_METATYPE(QStringListMap)
Q_DECLARE_METATYPE(PropMap)

struct SystemdUnitDBusMessage
Expand Down
2 changes: 2 additions & 0 deletions tests/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ void registerComplexDbusType() // FIXME: test shouldn't associate with DBus
qRegisterMetaType<ObjectMap>();
qDBusRegisterMetaType<ObjectMap>();
qDBusRegisterMetaType<QStringMap>();
qRegisterMetaType<QStringListMap>();
qDBusRegisterMetaType<QStringListMap>();
qRegisterMetaType<PropMap>();
qDBusRegisterMetaType<PropMap>();
qDBusRegisterMetaType<QDBusObjectPath>();
Expand Down
53 changes: 53 additions & 0 deletions tests/ut_desktopentry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,59 @@ TEST_F(TestDesktopEntry, prase)
EXPECT_TRUE(localeString == "文本编辑器");
}

TEST(DesktopFileParser, keywordsKeepLocales)
{
QTemporaryFile file;
ASSERT_TRUE(file.open());
ASSERT_GT(file.write("[Desktop Entry]\n"
"Type=Application\n"
"Name=Keyword Test\n"
"Keywords=Picture;Slideshow;Graphics;\n"
"Keywords[zh_CN]=图片;幻灯片;\n"),
0);
ASSERT_TRUE(file.flush());
ASSERT_TRUE(file.seek(0));

DesktopEntry entry;
ASSERT_EQ(entry.parse(file), ParserError::NoError);

auto value = entry.value("Desktop Entry", "Keywords");
ASSERT_TRUE(value.has_value());
const auto &keywords = value->get().value<QStringMap>();
EXPECT_EQ(keywords.value("default"), "Picture;Slideshow;Graphics;");
EXPECT_EQ(keywords.value("zh_CN"), QString::fromUtf8("图片;幻灯片;"));

const auto keywordMap = toKeywordMap(value->get());
EXPECT_EQ(keywordMap.size(), 2);
EXPECT_EQ(keywordMap.value("default"),
QStringList({"Picture", "Slideshow", "Graphics"}));
EXPECT_EQ(keywordMap.value("zh_CN"),
QStringList({QString::fromUtf8("图片"), QString::fromUtf8("幻灯片")}));
EXPECT_FALSE(keywordMap.contains("de"));
}

TEST(DesktopFileParser, keywordsSplitAndUnescape)
{
const QStringMap keywords{{"default", R"(Semi\;colon;Space\sKeyword;Backslash\\Keyword;)"}};
const auto keywordMap = toKeywordMap(QVariant::fromValue(keywords));
EXPECT_EQ(keywordMap.value("default"),
QStringList({"Semi;colon", "Space Keyword", R"(Backslash\Keyword)"}));
}

TEST(DesktopFileParser, keywordsPreserveLocaleKeys)
{
const QStringMap keywords{
{"default", "scan;flatbed;"},
{"de", "einlesen;Flachbett;"},
{"zh_CN", QString::fromUtf8("扫描;扫描仪;")},
};
const auto keywordMap = toKeywordMap(QVariant::fromValue(keywords));
EXPECT_EQ(keywordMap.value("default"), QStringList({"scan", "flatbed"}));
EXPECT_EQ(keywordMap.value("de"), QStringList({"einlesen", "Flachbett"}));
EXPECT_EQ(keywordMap.value("zh_CN"),
QStringList({QString::fromUtf8("扫描"), QString::fromUtf8("扫描仪")}));
}

TEST(DesktopFileParser, desktopEntryMustBeFirstGroup)
{
QTemporaryFile file;
Expand Down
Loading