/*
    SPDX-FileCopyrightText: 2013-2014 Sebastian Kügler <sebas@kde.org>
    SPDX-FileCopyrightText: 2014 Alex Richardson <arichardson.kde@gmail.com>

    SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/

#include "desktopfileparser_p.h"

#ifdef BUILDING_DESKTOPTOJSON_TOOL
#include "desktoptojson_debug.h"
#else
#include "desktopfileparser_debug.h"
#endif
// Qt
#include <QCache>
#include <QDir>
#include <QFile>
#include <QJsonArray>
#include <QJsonObject>
#include <QMutex>
#include <QRegularExpression>
#include <QRegularExpressionMatch>

#ifdef BUILDING_DESKTOPTOJSON_TOOL
// use if not else to prevent wrong scoping
#define DESKTOPTOJSON_VERBOSE_DEBUG                                                                                                                            \
    if (!DesktopFileParser::s_verbose) {                                                                                                                       \
    } else                                                                                                                                                     \
        qCDebug(DESKTOPPARSER)
#define DESKTOPTOJSON_VERBOSE_WARNING                                                                                                                          \
    if (!DesktopFileParser::s_verbose) {                                                                                                                       \
    } else                                                                                                                                                     \
        qCWarning(DESKTOPPARSER)
#else
#define DESKTOPTOJSON_VERBOSE_DEBUG QT_NO_QDEBUG_MACRO()
#define DESKTOPTOJSON_VERBOSE_WARNING QT_NO_QDEBUG_MACRO()
#endif

#include "../lib/kcoreaddons_export.h"

using namespace DesktopFileParser;

// This code was taken from KConfigGroupPrivate::deserializeList
QStringList DesktopFileParser::deserializeList(const QString &data, char separator)
{
    if (data.isEmpty()) {
        return QStringList();
    }
    if (data == QLatin1String("\\0")) {
        return QStringList(QString());
    }
    QStringList value;
    QString val;
    val.reserve(data.size());
    bool quoted = false;
    for (int p = 0; p < data.length(); p++) {
        if (quoted) {
            val += data[p];
            quoted = false;
        } else if (data[p].unicode() == '\\') {
            quoted = true;
        } else if (data[p].unicode() == separator) {
            value.append(val);
            if (p == data.length() - 1) {
                // don't add an empty entry to the end if the last character is a separator
                return value;
            }
            val.clear();
            val.reserve(data.size() - p);
        } else {
            val += data[p];
        }
    }
    value.append(val);
    return value;
}

QByteArray DesktopFileParser::escapeValue(const QByteArray &input)
{
    const int start = input.indexOf('\\');
    if (start < 0) {
        return input;
    }

    // we could do this in place, but this code is simpler
    // this tool is probably only transitional, so no need to optimize
    QByteArray result;
    result.reserve(input.size());
    result.append(input.data(), start);
    for (int i = start; i < input.length(); ++i) {
        if (input[i] != '\\') {
            result.append(input[i]);
        } else {
            if (i + 1 >= input.length()) {
                // just append the backslash if we are at end of line
                result.append(input[i]);
                break;
            }
            i++; // consume next character
            char nextChar = input[i];
            switch (nextChar) {
            case 's':
                result.append(' ');
                break;
            case 'n':
                result.append('\n');
                break;
            case 't':
                result.append('\t');
                break;
            case 'r':
                result.append('\r');
                break;
            case '\\':
                result.append('\\');
                break;
            default:
                result.append('\\');
                result.append(nextChar); // just ignore the escape sequence
            }
        }
    }
    return result;
}

struct CustomPropertyDefinition {
    // default ctor needed for QVector
    CustomPropertyDefinition()
        : type(QVariant::String)
    {
    }
    CustomPropertyDefinition(const QByteArray &key, QVariant::Type type)
        : key(key)
        , type(type)
    {
    }
    QJsonValue fromString(const QString &str) const
    {
        switch (type) {
        case QVariant::String:
            return str;
        case QVariant::StringList:
            return QJsonArray::fromStringList(deserializeList(str));
        case QVariant::Int: {
            bool ok = false;
            int result = str.toInt(&ok);
            if (!ok) {
                qCWarning(DESKTOPPARSER) << "Invalid integer value for key" << key << "-" << str;
                return QJsonValue();
            }
            return QJsonValue(result);
        }
        case QVariant::Double: {
            bool ok = false;
            double result = str.toDouble(&ok);
            if (!ok) {
                qCWarning(DESKTOPPARSER) << "Invalid double value for key" << key << "-" << str;
                return QJsonValue();
            }
            return QJsonValue(result);
        }
        case QVariant::Bool: {
            bool result = str.compare(QLatin1String("true"), Qt::CaseInsensitive) == 0;
            if (!result && str.compare(QLatin1String("false"), Qt::CaseInsensitive) != 0) {
                qCWarning(DESKTOPPARSER) << "Invalid boolean value for key" << key << "-" << str;
                return QJsonValue();
            }
            return QJsonValue(result);
        }
        default:
            // This was checked when parsing the file, no other QVariant::Type values are possible
            Q_UNREACHABLE();
        }
    }
    QByteArray key;
    QVariant::Type type;
};

namespace
{
bool readUntilDesktopEntryGroup(QFile &file, const QString &path, int &lineNr)
{
    if (!file.open(QFile::ReadOnly)) {
        qCWarning(DESKTOPPARSER) << "Error: Failed to open " << path;
        return false;
    }
    // we only convert data inside the [Desktop Entry] group
    while (!file.atEnd()) {
        const QByteArray line = file.readLine().trimmed();
        lineNr++;
        if (line == "[Desktop Entry]") {
            return true;
        }
    }
    qCWarning(DESKTOPPARSER) << "Error: Could not find [Desktop Entry] group in " << path;
    return false;
}

QByteArray readTypeEntryForCurrentGroup(QFile &df, QByteArray *nextGroup, QByteArray *pName)
{
    QByteArray group = *nextGroup;
    QByteArray type;
    if (group.isEmpty()) {
        qCWarning(DESKTOPPARSER, "Read empty .desktop file group name! Invalid file?");
    }
    while (!df.atEnd()) {
        QByteArray line = df.readLine().trimmed();
        // skip empty lines and comments
        if (line.isEmpty() || line.startsWith('#')) {
            continue;
        }
        if (line.startsWith('[')) {
            if (!line.endsWith(']')) {
                qCWarning(DESKTOPPARSER) << "Illegal .desktop group definition (does not end with ']'):" << line;
            }
            QByteArray name = line.mid(1, line.lastIndexOf(']') - 1).trimmed();
            // we have reached the next group -> return current group and Type= value
            *nextGroup = name;
            break;
        }

        const static QRegularExpression typeEntryRegex(QStringLiteral("^Type\\s*=\\s*(.*)$"));
        const auto match = typeEntryRegex.match(QString::fromUtf8(line));
        if (match.hasMatch()) {
            type = match.captured(1).toUtf8();
        } else if (pName) {
            const static QRegularExpression nameEntryRegex(QStringLiteral("^X-KDE-ServiceType\\s*=\\s*(.*)$"));
            const auto nameMatch = nameEntryRegex.match(QString::fromUtf8(line));
            if (nameMatch.hasMatch()) {
                *pName = nameMatch.captured(1).toUtf8();
            }
        }
    }
    return type;
}

bool tokenizeKeyValue(QFile &df, const QString &src, QByteArray &key, QString &value, int &lineNr)
{
    const QByteArray line = df.readLine().trimmed();
    lineNr++;
    if (line.isEmpty()) {
        DESKTOPTOJSON_VERBOSE_DEBUG << "Line " << lineNr << ": empty";
        return true;
    }
    if (line.startsWith('#')) {
        DESKTOPTOJSON_VERBOSE_DEBUG << "Line " << lineNr << ": comment";
        return true; // skip comments
    }
    if (line.startsWith('[')) {
        // start of new group -> doesn't interest us anymore
        DESKTOPTOJSON_VERBOSE_DEBUG << "Line " << lineNr << ": start of new group " << line;
        return false;
    }
    // must have form key=value now
    const int equalsIndex = line.indexOf('=');
    if (equalsIndex == -1) {
        qCWarning(DESKTOPPARSER).nospace() << qPrintable(src) << ':' << lineNr
                                           << ": Line is neither comment nor group "
                                              "and doesn't contain an '=' character: \""
                                           << line.constData() << '\"';
        return true;
    }
    // trim key and value to remove spaces around the '=' char
    key = line.mid(0, equalsIndex).trimmed();
    if (key.isEmpty()) {
        qCWarning(DESKTOPPARSER).nospace() << qPrintable(src) << ':' << lineNr << ": Key name is missing: \"" << line.constData() << '\"';
        return true;
    }

    const QByteArray valueRaw = line.mid(equalsIndex + 1).trimmed();
    const QByteArray valueEscaped = escapeValue(valueRaw);
    value = QString::fromUtf8(valueEscaped);

#ifdef BUILDING_DESKTOPTOJSON_TOOL
    DESKTOPTOJSON_VERBOSE_DEBUG.nospace() << "Line " << lineNr << ": key=" << key << ", value=" << value;
    if (valueEscaped != valueRaw) {
        DESKTOPTOJSON_VERBOSE_DEBUG << "Line " << lineNr << " contained escape sequences";
    }
#endif

    return true;
}

#define KSERVICETYPES_SUBDIR "kservicetypes" QT_STRINGIFY(QT_VERSION_MAJOR) "/"

static QString locateRelativeServiceType(const QString &relPath, const QStringList &serviceTypeSearchPaths)
{
    for (const auto &dir : serviceTypeSearchPaths) {
        QString path = dir + QLatin1String("/" KSERVICETYPES_SUBDIR) + relPath;
        if (QFile::exists(path)) {
            return path;
        }
    }
    return QString();
}

static ServiceTypeDefinition *parseServiceTypesFile(const QString &inputPath, const QStringList &serviceTypeSearchPaths)
{
    int lineNr = 0;
    QString path = inputPath;
    if (QDir::isRelativePath(path)) {
        path = locateRelativeServiceType(path, serviceTypeSearchPaths);
        QString rcPath;
        if (path.isEmpty()) {
            rcPath = QLatin1String(":/" KSERVICETYPES_SUBDIR) + inputPath;
            if (QFileInfo::exists(rcPath)) {
                path = rcPath;
            }
        }
        if (path.isEmpty()) {
            qCWarning(DESKTOPPARSER).nospace() << "Could not locate service type file " << KSERVICETYPES_SUBDIR << qPrintable(inputPath) << ", tried "
                                               << serviceTypeSearchPaths << " and " << rcPath;
            return nullptr;
        }
    }
    QFile df(path);
    if (!df.exists()) {
        qCCritical(DESKTOPPARSER) << "Service type file" << path << "does not exist";
        return nullptr;
    }
    if (!readUntilDesktopEntryGroup(df, path, lineNr)) {
        return nullptr;
    }
    ServiceTypeDefinition result;
    // TODO: passing nextGroup by pointer is inefficient as it will make deep copies every time
    // Not exactly performance critical code though so low priority
    QByteArray nextGroup = "Desktop Entry";
    // Type must be ServiceType now
    QByteArray typeStr = readTypeEntryForCurrentGroup(df, &nextGroup, &result.m_serviceTypeName);
    if (typeStr != "ServiceType") {
        qCWarning(DESKTOPPARSER) << path << "is not a valid service type: Type entry should be 'ServiceType', got" << typeStr << "instead.";
        return nullptr;
    }
    while (!df.atEnd()) {
        QByteArray currentGroup = nextGroup;
        typeStr = readTypeEntryForCurrentGroup(df, &nextGroup, nullptr);
        if (!currentGroup.startsWith("PropertyDef::")) {
            qCWarning(DESKTOPPARSER) << "Skipping invalid group" << currentGroup << "in service type" << path;
            continue;
        }
        if (typeStr.isEmpty()) {
            qCWarning(DESKTOPPARSER) << "Could not find Type= key in group" << currentGroup;
            continue;
        }
        QByteArray propertyName = currentGroup.mid(qstrlen("PropertyDef::"));
        QVariant::Type type = QVariant::nameToType(typeStr.constData());
        switch (type) {
        case QVariant::String:
        case QVariant::StringList:
        case QVariant::Int:
        case QVariant::Double:
        case QVariant::Bool:
            qCDebug(DESKTOPPARSER) << "Found property definition" << propertyName << "with type" << typeStr;
            result.m_propertyDefs.push_back(CustomPropertyDefinition(propertyName, type));
            break;
        case QVariant::Invalid:
            qCWarning(DESKTOPPARSER) << "Property type" << typeStr
                                     << "is not a known QVariant type."
                                        " Found while parsing property definition for"
                                     << propertyName << "in" << path;
            break;
        default:
            qCWarning(DESKTOPPARSER) << "Unsupported property type" << typeStr << "for property" << propertyName << "found in" << path
                                     << "\nOnly QString, QStringList, int, double and bool are supported.";
        }
    }
    return new ServiceTypeDefinition(result);
}

// a lazy map of service type definitions
typedef QCache<QString /*path*/, ServiceTypeDefinition> ServiceTypesHash;
Q_GLOBAL_STATIC(ServiceTypesHash, s_serviceTypes)
// access must be guarded by serviceTypesMutex as this code could be executed by multiple threads
QBasicMutex s_serviceTypesMutex;
} // end of anonymous namespace

ServiceTypeDefinitions ServiceTypeDefinitions::fromFiles(const QStringList &paths, const QStringList &serviceTypesSearchPaths)
{
    ServiceTypeDefinitions ret;
    ret.m_definitions.reserve(paths.size());
    // as we might modify the cache we need to acquire a mutex here
    for (const QString &serviceTypePath : paths) {
        bool added = ret.addFile(serviceTypePath, serviceTypesSearchPaths);
        if (!added) {
#ifdef BUILDING_DESKTOPTOJSON_TOOL
            exit(1); // this is a fatal error when using kcoreaddons_desktop_to_json()
#endif
        }
    }
    return ret;
}

bool ServiceTypeDefinitions::addFile(const QString &path, const QStringList &serviceTypesSearchPaths)
{
    QMutexLocker lock(&s_serviceTypesMutex);
    ServiceTypeDefinition *def = s_serviceTypes->object(path);

    if (def) {
        // in cache but we still must make our own copy
        m_definitions << *def;
    } else {
        // not found in cache -> we need to parse the file
        qCDebug(DESKTOPPARSER) << "About to parse service type file" << path;
        def = parseServiceTypesFile(path, serviceTypesSearchPaths);
        if (!def) {
            return false;
        }

        m_definitions << *def; // This must *precede* insert call, insert might delete
        s_serviceTypes->insert(path, def);
    }
    return true;
}

QJsonValue ServiceTypeDefinitions::parseValue(const QByteArray &aKey, const QString &value) const
{
    QByteArray key = aKey;
    // Remove translation markers "X-KDE-Keywords[en_GB]"
    const int startIdx = key.indexOf('[');
    if (startIdx != -1) {
        Q_ASSERT(key.endsWith(']'));
        key.truncate(startIdx);
    }

    auto findKey = [&key](const ServiceTypeDefinition &def) {
        return std::find_if(def.m_propertyDefs.cbegin(), def.m_propertyDefs.cend(), [&key](const CustomPropertyDefinition &propertyDef) {
            return propertyDef.key == key;
        });
    };

    // check whether the key has a special type associated with it
    for (const auto &def : m_definitions) {
        auto it = findKey(def);
        if (it != def.m_propertyDefs.cend()) {
            return it->fromString(value);
        }
    }
    qCDebug(DESKTOPPARSER) << "Unknown property type for key" << key << "-> falling back to string";
    return QJsonValue(value);
}

bool ServiceTypeDefinitions::hasServiceType(const QByteArray &serviceTypeName) const
{
    const auto it = std::find_if(m_definitions.begin(), m_definitions.end(), [&serviceTypeName](const ServiceTypeDefinition &def) {
        return def.m_serviceTypeName == serviceTypeName;
    });
    return it != m_definitions.end();
}

void DesktopFileParser::convertToJson(const QByteArray &key,
                                      ServiceTypeDefinitions &serviceTypes,
                                      const QString &value,
                                      QJsonObject &json,
                                      QJsonObject &kplugin,
                                      int lineNr)
{
    /* The following keys are recognized (and added to a "KPlugin" object):

        Icon=mypluginicon
        Type=Service
        ServiceTypes=KPluginInfo
        MimeType=text/plain;image/png

        Name=User Visible Name (translatable)
        Comment=Description of what the plugin does (translatable)

        X-KDE-PluginInfo-Author=Author's Name
        X-KDE-PluginInfo-Email=author@foo.bar
        # alternatively to X-KDE-PluginInfo-Author & X-KDE-PluginInfo-Email:
        X-KDE-PluginInfo-Authors=Author A's Name;AuthorB's Name (since KF 5.77)
        X-KDE-PluginInfo-Emails=authorA@foo.bar;authorB@foo.bar (since KF 5.77)
        X-KDE-PluginInfo-Name=internalname
        X-KDE-PluginInfo-Version=1.1
        X-KDE-PluginInfo-Website=http://www.plugin.org/
        X-KDE-PluginInfo-Category=playlist
        X-KDE-PluginInfo-License=GPL
        X-KDE-PluginInfo-Copyright=Copyright <year> by Author's Name (since KF 5.77, translatable)
        X-KDE-PluginInfo-EnabledByDefault=true
        X-KDE-FormFactors=desktop
    */
    if (key == "Icon") {
        kplugin[QStringLiteral("Icon")] = value;
    } else if (key == "X-KDE-PluginInfo-Name") {
        kplugin[QStringLiteral("Id")] = value;
    } else if (key == "X-KDE-PluginInfo-Category") {
        kplugin[QStringLiteral("Category")] = value;
    } else if (key == "X-KDE-PluginInfo-License") {
        kplugin[QStringLiteral("License")] = value;
    } else if (key == "X-KDE-PluginInfo-Copyright") {
        kplugin[QStringLiteral("Copyright")] = value;
    } else if (key.startsWith("X-KDE-PluginInfo-Copyright[")) {
        const QString languageSuffix = QString::fromUtf8(key.mid(qstrlen("X-KDE-PluginInfo-Copyright")));
        kplugin[QStringLiteral("Copyright") + languageSuffix] = value;
    } else if (key == "X-KDE-PluginInfo-Version") {
        kplugin[QStringLiteral("Version")] = value;
    } else if (key == "X-KDE-PluginInfo-Website") {
        kplugin[QStringLiteral("Website")] = value;
    }
#if KCOREADDONS_BUILD_DEPRECATED_SINCE(5, 79)
    else if (key == "X-KDE-PluginInfo-Depends") {
        kplugin[QStringLiteral("Dependencies")] = QJsonArray::fromStringList(deserializeList(value));
        qCDebug(DESKTOPPARSER) << "The X-KDE-PluginInfo-Depends property is deprecated and will be removed in KF6";
    }
#endif
    else if (key == "X-KDE-ServiceTypes" || key == "ServiceTypes") {
        // NOTE: "X-KDE-ServiceTypes" and "ServiceTypes" were already managed in the first parse step, so this second one is almost a noop
        const auto services = deserializeList(value);
        kplugin[QStringLiteral("ServiceTypes")] = QJsonArray::fromStringList(services);
    } else if (key == "MimeType") {
        // MimeType is a XDG string list and not a KConfig list so we need to use ';' as the separator
        kplugin[QStringLiteral("MimeTypes")] = QJsonArray::fromStringList(deserializeList(value, ';'));
        // make sure that applications using kcoreaddons_desktop_to_json() that depend on reading
        // the MimeType property still work (see https://git.reviewboard.kde.org/r/125527/)
        json[QStringLiteral("MimeType")] = value; // TODO KF6 remove this compatibility code
    } else if (key == "X-KDE-FormFactors") {
        kplugin[QStringLiteral("FormFactors")] = QJsonArray::fromStringList(deserializeList(value));
    } else if (key == "X-KDE-PluginInfo-EnabledByDefault") {
        bool boolValue = false;
        // should only be lower case, but be tolerant here
        if (value.toLower() == QLatin1String("true")) {
            boolValue = true;
        } else {
            if (value.toLower() != QLatin1String("false")) {
                qCWarning(DESKTOPPARSER).nospace() << "Expected boolean value for key \"" << key << "\" at line " << lineNr << "but got \"" << value
                                                   << "\" instead.";
            }
        }
        kplugin[QStringLiteral("EnabledByDefault")] = boolValue;
    } else if (key == "X-KDE-PluginInfo-Author") {
        QJsonObject authorsObject = kplugin.value(QStringLiteral("Authors")).toArray().at(0).toObject();
        // if the authors object doesn't exist yet this will create it
        authorsObject[QStringLiteral("Name")] = value;
        QJsonArray array;
        array.append(authorsObject);
        kplugin[QStringLiteral("Authors")] = array;
    } else if (key == "X-KDE-PluginInfo-Email") {
        QJsonObject authorsObject = kplugin.value(QStringLiteral("Authors")).toArray().at(0).toObject();
        // if the authors object doesn't exist yet this will create it
        authorsObject[QStringLiteral("Email")] = value;
        QJsonArray array;
        array.append(authorsObject);
        kplugin[QStringLiteral("Authors")] = array;
    } else if (key == "X-KDE-PluginInfo-Authors") {
        const auto authors = deserializeList(value);
        QJsonArray oldArray = kplugin.value(QStringLiteral("Authors")).toArray();
        QJsonArray newArray;
        for (int i = 0; i < authors.size(); ++i) {
            QJsonObject authorsObject = oldArray.at(i).toObject();
            // if the authors object doesn't exist yet this will create it
            authorsObject[QStringLiteral("Name")] = authors[i];
            newArray.append(authorsObject);
        }
        kplugin[QStringLiteral("Authors")] = newArray;
    } else if (key == "X-KDE-PluginInfo-Emails") {
        const auto emails = deserializeList(value);
        QJsonArray oldArray = kplugin.value(QStringLiteral("Authors")).toArray();
        QJsonArray newArray;
        for (int i = 0; i < emails.size(); ++i) {
            QJsonObject authorsObject = oldArray.at(i).toObject();
            // if the authors object doesn't exist yet this will create it
            authorsObject[QStringLiteral("Email")] = emails[i];
            newArray.append(authorsObject);
        }
        kplugin[QStringLiteral("Authors")] = newArray;
    } else if (key == "Name" || key.startsWith("Name[")) {
        // TODO: also handle GenericName? does that make any sense, or is X-KDE-PluginInfo-Category enough?
        kplugin[QString::fromUtf8(key)] = value;
    } else if (key == "Comment") {
        kplugin[QStringLiteral("Description")] = value;
    } else if (key.startsWith("Comment[")) {
        kplugin[QStringLiteral("Description") + QString::fromUtf8(key.mid(qstrlen("Comment")))] = value;
    } else if (key == "InitialPreference") {
        kplugin[QStringLiteral("InitialPreference")] = value.toInt();
    } else if (key == "Hidden") {
        DESKTOPTOJSON_VERBOSE_WARNING << "Hidden= key found in desktop file, this makes no sense"
                                         " with metadata inside the plugin.";
        kplugin[QString::fromUtf8(key)] = (value.toLower() == QLatin1String("true"));
    } else if (key == "X-KDE-BugReportUrl") {
        kplugin[QStringLiteral("BugReportUrl")] = value;
    } else if (key == "Exec" || key == "Type" || key == "Actions" || key == "X-KDE-Library" || key == "Encoding") {
        // Exec= doesn't make sense here, however some .desktop files (like e.g. in kdevelop) have a dummy value here
        // also the Type=Service entry is no longer needed
        // Actions= is used as hack at least with the Dolphin KPart to report the different view mode options
        // no plans yet to port that hack to JSON, so gently ignore it for now
        // X-KDE-Library is also not needed since we already have the library to read this metadata
        // Encoding= is also not converted as we always use utf-8 for reading
        DESKTOPTOJSON_VERBOSE_DEBUG << "Not converting key " << key << "=" << value;
    } else {
        // check service type definitions or fall back to QString
        json[QString::fromUtf8(key)] = serviceTypes.parseValue(key, value);
    }
}

bool DesktopFileParser::convert(const QString &src,
                                const QStringList &serviceTypes,
                                QJsonObject &json,
                                QString *libraryPath,
                                const QStringList &serviceTypesSearchPaths)
{
    QFile df(src);
    int lineNr = 0;
    ServiceTypeDefinitions serviceTypeDef = ServiceTypeDefinitions::fromFiles(serviceTypes, serviceTypesSearchPaths);
    readUntilDesktopEntryGroup(df, src, lineNr);
    DESKTOPTOJSON_VERBOSE_DEBUG << "Found [Desktop Entry] group in line" << lineNr;
    auto startPos = df.pos();

    // parse it a first time to know servicetype
    while (!df.atEnd()) {
        QByteArray key;
        QString value;
        if (!tokenizeKeyValue(df, src, key, value, lineNr)) {
            break;
        }
        // some .desktop files still use the legacy ServiceTypes= key
        if (key == "X-KDE-ServiceTypes" || key == "ServiceTypes") {
            const QString dotDesktop = QStringLiteral(".desktop");
            const QChar slashChar(QLatin1Char('/'));
            const auto serviceList = deserializeList(value);

            for (const auto &service : serviceList) {
                if (!serviceTypeDef.hasServiceType(service.toLatin1())) {
                    // Make up the filename from the service type name. This assumes consistent naming...
                    QString absFileName =
                        locateRelativeServiceType(service.toLower().replace(slashChar, QLatin1Char('-')) + dotDesktop, serviceTypesSearchPaths);
                    if (absFileName.isEmpty()) {
                        absFileName = locateRelativeServiceType(service.toLower().remove(slashChar) + dotDesktop, serviceTypesSearchPaths);
                    }
                    if (absFileName.isEmpty()) {
                        qCWarning(DESKTOPPARSER) << "Unable to find service type for service" << service << "listed in" << src
                                                 << "- service type search directories were" << serviceTypesSearchPaths;
                    } else {
                        serviceTypeDef.addFile(absFileName, serviceTypesSearchPaths);
                    }
                }
            }
            break;
        }
    }
    lineNr = 0;
    df.seek(startPos);

    QJsonObject kplugin; // the "KPlugin" key of the metadata
    while (!df.atEnd()) {
        QByteArray key;
        QString value;
        if (!tokenizeKeyValue(df, src, key, value, lineNr)) {
            break;
        } else if (key.isEmpty()) {
            continue;
        }
#ifdef BUILDING_DESKTOPTOJSON_TOOL
        if (s_compatibilityMode) {
            convertToCompatibilityJson(QString::fromUtf8(key), value, json, lineNr);
        } else {
            convertToJson(key, serviceTypeDef, value, json, kplugin, lineNr);
        }
#else
        convertToJson(key, serviceTypeDef, value, json, kplugin, lineNr);
#endif
        if (libraryPath && key == "X-KDE-Library") {
            *libraryPath = value;
        }
    }
    json[QStringLiteral("KPlugin")] = kplugin;
    return true;
}
