From 3988e0c5265e22c5fbf0e4d05060e72a81224a2d Mon Sep 17 00:00:00 2001 From: black-desk Date: Fri, 25 Aug 2023 16:04:55 +0800 Subject: [PATCH] refact: improve desktop parsing --- .clang-format | 2 +- src/dbus/applicationservice.cpp | 148 +++++------ src/desktopentry.cpp | 420 +++++++++++++++----------------- src/desktopentry.h | 23 +- src/global.h | 36 +-- 5 files changed, 308 insertions(+), 321 deletions(-) diff --git a/.clang-format b/.clang-format index 1421947..879896b 100644 --- a/.clang-format +++ b/.clang-format @@ -123,7 +123,7 @@ IncludeCategories: - Regex: '.*' Priority: 1 # 缩进case标签 -IndentCaseLabels: true +IndentCaseLabels: false #IndentPPDirectives: AfterHash # 缩进宽度 diff --git a/src/dbus/applicationservice.cpp b/src/dbus/applicationservice.cpp index fcc17d9..b5717c0 100644 --- a/src/dbus/applicationservice.cpp +++ b/src/dbus/applicationservice.cpp @@ -395,92 +395,92 @@ LaunchTask ApplicationService::unescapeExec(const QString &str, const QStringLis auto location = execList.indexOf(codeStr); switch (filesCode) { - case 'f': { // Defer to async job - task.command.append(std::move(execList)); - for (auto &field : fields) { - task.Resources.emplace_back(std::move(field)); - } - break; + case 'f': { // Defer to async job + task.command.append(std::move(execList)); + for (auto &field : fields) { + task.Resources.emplace_back(std::move(field)); } - case 'u': { - execList.removeAt(location); - if (fields.empty()) { - task.command.append(execList); - break; - } - if (fields.count() > 1) { - qDebug() << R"(fields count is greater than one, %u will only take first element.)"; - } - execList.insert(location, fields.first()); + break; + } + case 'u': { + execList.removeAt(location); + if (fields.empty()) { task.command.append(execList); break; } - case 'F': - [[fallthrough]]; - case 'U': { - execList.removeAt(location); - auto it = execList.begin() + location; - for (const auto &field : fields) { - it = execList.insert(it, field); - } - task.command.append(std::move(execList)); - break; + if (fields.count() > 1) { + qDebug() << R"(fields count is greater than one, %u will only take first element.)"; } - case 'i': { - execList.removeAt(location); - auto val = m_entry->value(DesktopFileEntryKey, "Icon"); - if (!val) { - qDebug() << R"(Application Icons can't be found. %i will be ignored.)"; - task.command.append(std::move(execList)); - return task; - } - bool ok; - auto iconStr = val->toIconString(ok); - if (!ok) { - qDebug() << R"(Icons Convert to string failed. %i will be ignored.)"; - task.command.append(std::move(execList)); - return task; - } - auto it = execList.insert(location, iconStr); - execList.insert(it, "--icon"); - task.command.append(std::move(execList)); - break; + execList.insert(location, fields.first()); + task.command.append(execList); + break; + } + case 'F': + [[fallthrough]]; + case 'U': { + execList.removeAt(location); + auto it = execList.begin() + location; + for (const auto &field : fields) { + it = execList.insert(it, field); } - case 'c': { - execList.removeAt(location); - auto val = m_entry->value(DesktopFileEntryKey, "Name"); - if (!val) { - qDebug() << R"(Application Name can't be found. %c will be ignored.)"; - task.command.append(std::move(execList)); - return task; - } - bool ok; - auto NameStr = val->toLocaleString(getUserLocale(), ok); - if (!ok) { - qDebug() << R"(Name Convert to locale string failed. %c will be ignored.)"; - task.command.append(std::move(execList)); - return task; - } - execList.insert(location, NameStr); + task.command.append(std::move(execList)); + break; + } + case 'i': { + execList.removeAt(location); + auto val = m_entry->value(DesktopFileEntryKey, "Icon"); + if (!val) { + qDebug() << R"(Application Icons can't be found. %i will be ignored.)"; task.command.append(std::move(execList)); - break; + return task; } - case 'k': { // ignore all desktop file location for now. - execList.removeAt(location); + bool ok; + auto iconStr = val->toIconString(ok); + if (!ok) { + qDebug() << R"(Icons Convert to string failed. %i will be ignored.)"; task.command.append(std::move(execList)); - break; + return task; } - case 'd': - case 'D': - case 'n': - case 'N': - case 'v': - [[fallthrough]]; // Deprecated field codes should be removed from the command line and ignored. - case 'm': { - execList.removeAt(location); + auto it = execList.insert(location, iconStr); + execList.insert(it, "--icon"); + task.command.append(std::move(execList)); + break; + } + case 'c': { + execList.removeAt(location); + auto val = m_entry->value(DesktopFileEntryKey, "Name"); + if (!val) { + qDebug() << R"(Application Name can't be found. %c will be ignored.)"; task.command.append(std::move(execList)); - break; + return task; } + bool ok; + auto NameStr = val->toLocaleString(getUserLocale(), ok); + if (!ok) { + qDebug() << R"(Name Convert to locale string failed. %c will be ignored.)"; + task.command.append(std::move(execList)); + return task; + } + execList.insert(location, NameStr); + task.command.append(std::move(execList)); + break; + } + case 'k': { // ignore all desktop file location for now. + execList.removeAt(location); + task.command.append(std::move(execList)); + break; + } + case 'd': + case 'D': + case 'n': + case 'N': + case 'v': + [[fallthrough]]; // Deprecated field codes should be removed from the command line and ignored. + case 'm': { + execList.removeAt(location); + task.command.append(std::move(execList)); + break; + } } if (task.Resources.isEmpty()) { diff --git a/src/desktopentry.cpp b/src/desktopentry.cpp index 878506d..65441f8 100644 --- a/src/desktopentry.cpp +++ b/src/desktopentry.cpp @@ -16,6 +16,24 @@ #include namespace { +bool isInvalidLocaleString(const QString &str) noexcept +{ + constexpr auto Language = R"((?:[a-z]+))"; // language of locale postfix. eg.(en, zh) + constexpr auto Country = R"((?:_[A-Z]+))"; // country of locale postfix. eg.(US, CN) + constexpr auto Encoding = R"((?:\.[0-9A-Z-]+))"; // encoding of locale postfix. eg.(UFT-8) + constexpr auto Modifier = R"((?:@[a-z=;]+))"; // modifier of locale postfix. eg.(euro;collation=traditional) + const static auto validKey = QString(R"(^%1%2?%3?%4?$)").arg(Language, Country, Encoding, Modifier); + // example: https://regex101.com/r/hylOay/2 + static const QRegularExpression _re = []() -> QRegularExpression { + QRegularExpression tmp{validKey}; + tmp.optimize(); + return tmp; + }(); + thread_local const auto re = _re; + + return re.match(str).hasMatch(); +} + bool hasNonAsciiAndControlCharacters(const QString &str) noexcept { static const QRegularExpression _matchControlChars = []() { @@ -36,89 +54,126 @@ bool hasNonAsciiAndControlCharacters(const QString &str) noexcept return false; } -} // namespace -auto DesktopEntry::parseGroupHeader(const QString &str) noexcept +struct Parser { + Parser(QTextStream &stream) + : m_stream(stream){}; + QTextStream &m_stream; + QString m_line; + + using Groups = QMap>; + + DesktopErrorCode parse(Groups &groups) noexcept; + +private: + void skip() noexcept; + DesktopErrorCode addGroup(Groups &groups) noexcept; + DesktopErrorCode addEntry(Groups::iterator &group) noexcept; +}; + +void Parser::skip() noexcept +{ + while (!m_stream.atEnd() and (m_line.startsWith('#') or m_line.isEmpty())) { + m_line = m_stream.readLine(); + } +} + +DesktopErrorCode Parser::parse(Groups &ret) noexcept +{ + std::remove_reference_t groups; + while (!m_stream.atEnd()) { + auto err = addGroup(groups); + if (err != DesktopErrorCode::NoError) { + return err; + } + + if (groups.size() != 1) { + continue; + } + + if (groups.keys().first() != DesktopFileEntryKey) { + qWarning() << "There should be nothing preceding " + "'Desktop Entry' group in the desktop entry file " + "but possibly one or more comments."; + return DesktopErrorCode::InvalidFormat; + } + } + + if (!m_line.isEmpty()) { + qCritical() << "Something is wrong in Desktop file parser, check logic."; + return DesktopErrorCode::InternalError; + } + + ret = std::move(groups); + return DesktopErrorCode::NoError; +} + +DesktopErrorCode Parser::addGroup(Groups &ret) noexcept +{ + skip(); + if (!m_line.startsWith('[')) { + qWarning() << "Invalid desktop file format: unexpected line:" << m_line; + return DesktopErrorCode::InvalidFormat; + } + + // Parsing group header. // https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#group-header - auto groupHeader = str.sliced(1, str.size() - 2).trimmed(); - decltype(m_entryMap)::iterator it{m_entryMap.end()}; + auto groupHeader = m_line.sliced(1, m_line.size() - 2).trimmed(); if (groupHeader.contains('[') || groupHeader.contains(']') || hasNonAsciiAndControlCharacters(groupHeader)) { - qWarning() << "group header invalid:" << str; - return it; + qWarning() << "group header invalid:" << m_line; + return DesktopErrorCode::InvalidFormat; } - auto tmp = m_entryMap.find(groupHeader); - if (tmp == m_entryMap.end()) { - it = m_entryMap.insert(groupHeader, {}); + if (ret.find(groupHeader) != ret.end()) { + qWarning() << "duplicated group header detected:" << groupHeader; + return DesktopErrorCode::InvalidFormat; } - qWarning() << "group header already exists:" << str; - return it; -} + auto group = ret.insert(groupHeader, {}); -QString DesktopFile::sourcePath() const noexcept -{ - if (!m_fileSource) { - return ""; - } - - QFileInfo info(*m_fileSource); - return info.absoluteFilePath(); -} - -bool DesktopEntry::isInvalidLocaleString(const QString &str) noexcept -{ - constexpr auto Language = R"((?:[a-z]+))"; // language of locale postfix. eg.(en, zh) - constexpr auto Country = R"((?:_[A-Z]+))"; // country of locale postfix. eg.(US, CN) - constexpr auto Encoding = R"((?:\.[0-9A-Z-]+))"; // encoding of locale postfix. eg.(UFT-8) - constexpr auto Modifier = R"((?:@[a-z=;]+))"; // modifier of locale postfix. eg.(euro;collation=traditional) - const static auto validKey = QString(R"(^%1%2?%3?%4?$)").arg(Language, Country, Encoding, Modifier); - // example: https://regex101.com/r/hylOay/2 - static const QRegularExpression _re = []() -> QRegularExpression { - QRegularExpression tmp{validKey}; - tmp.optimize(); - return tmp; - }(); - thread_local const auto re = _re; - - return re.match(str).hasMatch(); -} - -QPair DesktopEntry::processEntry(const QString &str) noexcept -{ - auto splitCharIndex = str.indexOf(']'); - if (splitCharIndex != -1) { - for (; splitCharIndex < str.size(); ++splitCharIndex) { - if (str.at(splitCharIndex) == '=') { - break; - } + m_line.clear(); + while (!m_stream.atEnd() && !m_line.startsWith('[')) { + skip(); + auto err = addEntry(group); + if (err != DesktopErrorCode::NoError) { + return err; } - } else { - splitCharIndex = str.indexOf('='); } - auto keyStr = str.first(splitCharIndex).trimmed(); - auto valueStr = str.sliced(splitCharIndex + 1).trimmed(); - return qMakePair(std::move(keyStr), std::move(valueStr)); + return DesktopErrorCode::NoError; } -std::optional> DesktopEntry::processEntryKey(const QString &keyStr) noexcept +DesktopErrorCode Parser::addEntry(Groups::iterator &group) noexcept { - QString key; - QString localeStr; + auto line = m_line; + m_line.clear(); + auto splitCharIndex = line.indexOf('='); + if (splitCharIndex == -1) { + qWarning() << "invalid line in desktop file, skip it:" << line; + return DesktopErrorCode::NoError; + } + auto keyStr = line.first(splitCharIndex).trimmed(); + auto valueStr = line.sliced(splitCharIndex + 1).trimmed(); + + QString key{""}; + QString localeStr{defaultKeyStr}; // NOTE: // We are process "localized keys" here, for usage check: // https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#localized-keys - if (auto index = keyStr.indexOf('['); index != -1) { - key = keyStr.sliced(0, index); - localeStr = keyStr.sliced(index + 1, keyStr.length() - 1 - index - 1); // strip '[' and ']' - if (!isInvalidLocaleString(localeStr)) { - qWarning().noquote() << QString("invalid LOCALE (%2) for key \"%1\"").arg(key, localeStr); - } - } else { + qsizetype localeBegin = keyStr.indexOf('['); + qsizetype localeEnd = keyStr.lastIndexOf(']'); + if ((localeBegin == -1 && localeEnd != -1) || (localeBegin != -1 && localeEnd == -1)) { + qWarning() << "unmatched [] detected in desktop file, skip this line: " << line; + return DesktopErrorCode::NoError; + } + + if (localeBegin == -1 && localeEnd == -1) { key = keyStr; + } else { + key = keyStr.sliced(0, localeBegin); + localeStr = keyStr.sliced(localeBegin + 1, localeEnd - localeBegin - 1); // strip '[' and ']' } static const QRegularExpression _re = []() { @@ -129,44 +184,34 @@ std::optional> DesktopEntry::processEntryKey(const QStri // NOTE: https://stackoverflow.com/a/25583104 thread_local const QRegularExpression re = _re; if (re.match(key).hasMatch()) { - qWarning() << "keyName's format is invalid."; - return std::nullopt; + qWarning() << "invalid key name, skip this line:" << line; + return DesktopErrorCode::NoError; } - return qMakePair(std::move(key), std::move(localeStr)); + if (localeStr != defaultKeyStr && !isInvalidLocaleString(localeStr)) { + qWarning().noquote() << QString("invalid LOCALE (%2) for key \"%1\"").arg(key, localeStr); + } + + auto keyIt = group->find(key); + if (keyIt != group->end() && keyIt->find(localeStr) != keyIt->end()) { + qWarning() << "duplicated localestring, skip this line:" << line; + return DesktopErrorCode::NoError; + } + + group->insert(key, {{localeStr, valueStr}}); + return DesktopErrorCode::NoError; } -DesktopErrorCode DesktopEntry::parseEntry(const QString &str, decltype(m_entryMap)::iterator ¤tGroup) noexcept +} // namespace + +QString DesktopFile::sourcePath() const noexcept { - auto [key, value] = processEntry(str); - auto keyPair = processEntryKey(key); - - if (!keyPair.has_value()) { - return DesktopErrorCode::InvalidFormat; + if (!m_fileSource) { + return ""; } - auto [keyName, localeStr] = std::move(keyPair).value(); - if (localeStr.isEmpty()) { - localeStr = defaultKeyStr; - } - - auto valueIt = currentGroup->find(keyName); - if (valueIt == currentGroup->end()) { - currentGroup->insert(keyName, {{localeStr, value}}); - return DesktopErrorCode::NoError; - } - - auto innerIt = valueIt->find(localeStr); - if (innerIt == valueIt->end()) { - valueIt->insert(localeStr, value); - return DesktopErrorCode::NoError; - } - - qWarning() << "duplicated postfix and this line will be aborted, maybe format is invalid.\n" - << "exist: " << innerIt.key() << "[" << innerIt.value() << "]" - << "current: " << keyName << "[" << localeStr << "]"; - - return DesktopErrorCode::NoError; + QFileInfo info(*m_fileSource); + return info.absoluteFilePath(); } bool DesktopEntry::checkMainEntryValidation() const noexcept @@ -333,86 +378,26 @@ DesktopErrorCode DesktopEntry::parse(DesktopFile &file) noexcept return parse(stream); } -bool DesktopEntry::skipCheck(const QString &line) noexcept -{ - return line.startsWith('#') or line.isEmpty(); -} - DesktopErrorCode DesktopEntry::parse(QTextStream &stream) noexcept { + if (m_parsed == true) { + return DesktopErrorCode::Parsed; + } + if (stream.atEnd()) { - if (m_context == EntryContext::Done) { - return DesktopErrorCode::NoError; - } return DesktopErrorCode::OpenFailed; } stream.setEncoding(QStringConverter::Utf8); - decltype(m_entryMap)::iterator currentGroup; DesktopErrorCode err{DesktopErrorCode::NoError}; - bool mainEntryParsed{false}; - QString line; - - while (!stream.atEnd()) { - switch (m_context) { - case EntryContext::Unknown: { - qWarning() << "entry context is unknown,abort."; - err = DesktopErrorCode::InvalidFormat; - return err; - } break; - case EntryContext::EntryOuter: { - if (skipCheck(line)) { - line = stream.readLine().trimmed(); - continue; - } - - if (line.startsWith('[')) { - auto group = parseGroupHeader(line); - - if (group == m_entryMap.end()) { - return DesktopErrorCode::InvalidFormat; - } - currentGroup = group; - bool isMainEntry = (currentGroup.key() == DesktopFileEntryKey); - - if ((!mainEntryParsed and isMainEntry) or (mainEntryParsed and !isMainEntry)) { - m_context = EntryContext::Entry; - continue; - } - } - qWarning() << "groupName format error:" << line; - err = DesktopErrorCode::InvalidFormat; - return err; - } break; - case EntryContext::Entry: { - line = stream.readLine().trimmed(); - - if (skipCheck(line)) { - continue; - } - - if (line.startsWith('[')) { - m_context = EntryContext::EntryOuter; - - if (currentGroup.key() == DesktopFileEntryKey) { - mainEntryParsed = true; - } - continue; - } - - err = parseEntry(line, currentGroup); - if (err != DesktopErrorCode::NoError) { - qWarning() << "Entry format error:" << line; - return err; - } - } break; - case EntryContext::Done: - break; - } + Parser p(stream); + err = p.parse(m_entryMap); + m_parsed = true; + if (err != DesktopErrorCode::NoError) { + return err; } - m_context = EntryContext::Done; if (!checkMainEntryValidation()) { qWarning() << "invalid MainEntry, abort."; err = DesktopErrorCode::MissingInfo; @@ -460,33 +445,33 @@ QString DesktopEntry::Value::unescape(const QString &str) noexcept } 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': - unescapedStr.append(' '); - ++i; - break; + 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': + unescapedStr.append(' '); + ++i; + break; } } @@ -562,34 +547,33 @@ QDebug operator<<(QDebug debug, const DesktopErrorCode &v) QDebugStateSaver saver{debug}; QString errMsg; switch (v) { - case DesktopErrorCode::NoError: { - errMsg = "no error."; - break; - } - case DesktopErrorCode::NotFound: { - errMsg = "file not found."; - break; - } - case DesktopErrorCode::MismatchedFile: { - errMsg = "file type is mismatched."; - break; - } - case DesktopErrorCode::InvalidLocation: { - errMsg = "file location is invalid, please check $XDG_DATA_DIRS."; - break; - } - case DesktopErrorCode::OpenFailed: { - errMsg = "couldn't open the file."; - break; - } - case DesktopErrorCode::InvalidFormat: { - errMsg = "the format of desktopEntry file is invalid."; - break; - } - case DesktopErrorCode::MissingInfo: { - errMsg = "missing required infomation."; - break; - } + case DesktopErrorCode::NoError: { + errMsg = "no error."; + } break; + case DesktopErrorCode::NotFound: { + errMsg = "file not found."; + } break; + case DesktopErrorCode::MismatchedFile: { + errMsg = "file type is mismatched."; + } break; + case DesktopErrorCode::InvalidLocation: { + errMsg = "file location is invalid, please check $XDG_DATA_DIRS."; + } break; + case DesktopErrorCode::OpenFailed: { + errMsg = "couldn't open the file."; + } break; + case DesktopErrorCode::InvalidFormat: { + errMsg = "the format of desktopEntry file is invalid."; + } break; + case DesktopErrorCode::MissingInfo: { + errMsg = "missing required infomation."; + } break; + case DesktopErrorCode::Parsed: { + errMsg = "this desktop entry is parsed."; + } break; + case DesktopErrorCode::InternalError: { + errMsg = "internal error of parser."; + } break; } debug << errMsg; return debug; diff --git a/src/desktopentry.h b/src/desktopentry.h index 769b460..52b9e31 100644 --- a/src/desktopentry.h +++ b/src/desktopentry.h @@ -15,7 +15,17 @@ constexpr static auto defaultKeyStr = "default"; -enum class DesktopErrorCode { NoError, NotFound, MismatchedFile, InvalidLocation, InvalidFormat, OpenFailed, MissingInfo }; +enum class DesktopErrorCode { + NoError, + NotFound, + MismatchedFile, + InvalidLocation, + InvalidFormat, + OpenFailed, + MissingInfo, + Parsed, + InternalError, +}; enum class EntryContext { Unknown, EntryOuter, Entry, Done }; @@ -124,18 +134,11 @@ public: [[nodiscard]] DesktopErrorCode parse(QTextStream &stream) noexcept; [[nodiscard]] std::optional> group(const QString &key) const noexcept; [[nodiscard]] std::optional value(const QString &key, const QString &valueKey) const noexcept; - static bool isInvalidLocaleString(const QString &str) noexcept; private: - EntryContext m_context{EntryContext::EntryOuter}; - QMap> m_entryMap; - - auto parseGroupHeader(const QString &str) noexcept; [[nodiscard]] bool checkMainEntryValidation() const noexcept; - static bool skipCheck(const QString &line) noexcept; - static DesktopErrorCode parseEntry(const QString &str, decltype(m_entryMap)::iterator ¤tGroup) noexcept; - static QPair processEntry(const QString &str) noexcept; - static std::optional> processEntryKey(const QString &keyStr) noexcept; + QMap> m_entryMap; + bool m_parsed{false}; }; QDebug operator<<(QDebug debug, const DesktopEntry::Value &v); diff --git a/src/global.h b/src/global.h index a955791..7cf3130 100644 --- a/src/global.h +++ b/src/global.h @@ -117,26 +117,26 @@ public: } switch (m_serverType) { - case DBusType::Session: - [[fallthrough]]; - case DBusType::System: { - m_serverConnection.emplace(QDBusConnection::connectToBus(static_cast(m_serverType), - ApplicationManagerServerDBusName)); - if (!m_serverConnection->isConnected()) { - qFatal("%s", m_serverConnection->lastError().message().toLocal8Bit().data()); - } - return m_serverConnection.value(); + case DBusType::Session: + [[fallthrough]]; + case DBusType::System: { + m_serverConnection.emplace(QDBusConnection::connectToBus(static_cast(m_serverType), + ApplicationManagerServerDBusName)); + if (!m_serverConnection->isConnected()) { + qFatal("%s", m_serverConnection->lastError().message().toLocal8Bit().data()); } - case DBusType::Custom: { - if (m_serverBusAddress.isEmpty()) { - qFatal("connect to custom dbus must init this object by custom dbus address"); - } - m_serverConnection.emplace(QDBusConnection::connectToBus(m_serverBusAddress, ApplicationManagerServerDBusName)); - if (!m_serverConnection->isConnected()) { - qFatal("%s", m_serverConnection->lastError().message().toLocal8Bit().data()); - } - return m_serverConnection.value(); + return m_serverConnection.value(); + } + case DBusType::Custom: { + if (m_serverBusAddress.isEmpty()) { + qFatal("connect to custom dbus must init this object by custom dbus address"); } + m_serverConnection.emplace(QDBusConnection::connectToBus(m_serverBusAddress, ApplicationManagerServerDBusName)); + if (!m_serverConnection->isConnected()) { + qFatal("%s", m_serverConnection->lastError().message().toLocal8Bit().data()); + } + return m_serverConnection.value(); + } } Q_UNREACHABLE();