KCoreAddons

desktopfileparser.cpp
1 /*
2  SPDX-FileCopyrightText: 2013-2014 Sebastian K├╝gler <[email protected]>
3  SPDX-FileCopyrightText: 2014 Alex Richardson <[email protected]>
4 
5  SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
6 */
7 
8 #include "desktopfileparser_p.h"
9 
10 #ifdef BUILDING_DESKTOPTOJSON_TOOL
11 #include "desktoptojson_debug.h"
12 #else
13 #include "desktopfileparser_debug.h"
14 #endif
15 // Qt
16 #include <QCache>
17 #include <QDir>
18 #include <QFile>
19 #include <QJsonArray>
20 #include <QJsonObject>
21 #include <QMutex>
22 #include <QRegularExpression>
23 #include <QRegularExpressionMatch>
24 #include <QStandardPaths>
25 
26 #ifdef BUILDING_DESKTOPTOJSON_TOOL
27 // use if not else to prevent wrong scoping
28 #define DESKTOPTOJSON_VERBOSE_DEBUG \
29  if (!DesktopFileParser::s_verbose) { \
30  } else \
31  qCDebug(DESKTOPPARSER)
32 #define DESKTOPTOJSON_VERBOSE_WARNING \
33  if (!DesktopFileParser::s_verbose) { \
34  } else \
35  qCWarning(DESKTOPPARSER)
36 #else
37 #define DESKTOPTOJSON_VERBOSE_DEBUG QT_NO_QDEBUG_MACRO()
38 #define DESKTOPTOJSON_VERBOSE_WARNING QT_NO_QDEBUG_MACRO()
39 #endif
40 
41 #include "../lib/kcoreaddons_export.h"
42 
43 using namespace DesktopFileParser;
44 
45 // This code was taken from KConfigGroupPrivate::deserializeList
46 QStringList DesktopFileParser::deserializeList(const QString &data, char separator)
47 {
48  if (data.isEmpty()) {
49  return QStringList();
50  }
51  if (data == QLatin1String("\\0")) {
52  return QStringList(QString());
53  }
54  QStringList value;
55  QString val;
56  val.reserve(data.size());
57  bool quoted = false;
58  for (int p = 0; p < data.length(); p++) {
59  if (quoted) {
60  val += data[p];
61  quoted = false;
62  } else if (data[p].unicode() == '\\') {
63  quoted = true;
64  } else if (data[p].unicode() == separator) {
65  value.append(val);
66  if (p == data.length() - 1) {
67  // don't add an empty entry to the end if the last character is a separator
68  return value;
69  }
70  val.clear();
71  val.reserve(data.size() - p);
72  } else {
73  val += data[p];
74  }
75  }
76  value.append(val);
77  return value;
78 }
79 
80 QByteArray DesktopFileParser::escapeValue(const QByteArray &input)
81 {
82  const int start = input.indexOf('\\');
83  if (start < 0) {
84  return input;
85  }
86 
87  // we could do this in place, but this code is simpler
88  // this tool is probably only transitional, so no need to optimize
89  QByteArray result;
90  result.reserve(input.size());
91  result.append(input.data(), start);
92  for (int i = start; i < input.length(); ++i) {
93  if (input[i] != '\\') {
94  result.append(input[i]);
95  } else {
96  if (i + 1 >= input.length()) {
97  // just append the backslash if we are at end of line
98  result.append(input[i]);
99  break;
100  }
101  i++; // consume next character
102  char nextChar = input[i];
103  switch (nextChar) {
104  case 's':
105  result.append(' ');
106  break;
107  case 'n':
108  result.append('\n');
109  break;
110  case 't':
111  result.append('\t');
112  break;
113  case 'r':
114  result.append('\r');
115  break;
116  case '\\':
117  result.append('\\');
118  break;
119  default:
120  result.append('\\');
121  result.append(nextChar); // just ignore the escape sequence
122  }
123  }
124  }
125  return result;
126 }
127 
128 struct CustomPropertyDefinition {
129  // default ctor needed for QVector
130  CustomPropertyDefinition()
131  : type(QVariant::String)
132  {
133  }
134  CustomPropertyDefinition(const QByteArray &key, QVariant::Type type)
135  : key(key)
136  , type(type)
137  {
138  }
139  QJsonValue fromString(const QString &str) const
140  {
141  switch (type) {
142  case QVariant::String:
143  return str;
146  case QVariant::Int: {
147  bool ok = false;
148  int result = str.toInt(&ok);
149  if (!ok) {
150  qCWarning(DESKTOPPARSER) << "Invalid integer value for key" << key << "-" << str;
151  return QJsonValue();
152  }
153  return QJsonValue(result);
154  }
155  case QVariant::Double: {
156  bool ok = false;
157  double result = str.toDouble(&ok);
158  if (!ok) {
159  qCWarning(DESKTOPPARSER) << "Invalid double value for key" << key << "-" << str;
160  return QJsonValue();
161  }
162  return QJsonValue(result);
163  }
164  case QVariant::Bool: {
165  bool result = str.compare(QLatin1String("true"), Qt::CaseInsensitive) == 0;
166  if (!result && str.compare(QLatin1String("false"), Qt::CaseInsensitive) != 0) {
167  qCWarning(DESKTOPPARSER) << "Invalid boolean value for key" << key << "-" << str;
168  return QJsonValue();
169  }
170  return QJsonValue(result);
171  }
172  default:
173  // This was checked when parsing the file, no other QVariant::Type values are possible
174  Q_UNREACHABLE();
175  }
176  }
177  QByteArray key;
178  QVariant::Type type;
179 };
180 
181 namespace
182 {
183 bool readUntilDesktopEntryGroup(QFile &file, const QString &path, int &lineNr)
184 {
185  if (!file.open(QFile::ReadOnly)) {
186  qCWarning(DESKTOPPARSER) << "Error: Failed to open " << path;
187  return false;
188  }
189  // we only convert data inside the [Desktop Entry] group
190  while (!file.atEnd()) {
191  const QByteArray line = file.readLine().trimmed();
192  lineNr++;
193  if (line == "[Desktop Entry]") {
194  return true;
195  }
196  }
197  qCWarning(DESKTOPPARSER) << "Error: Could not find [Desktop Entry] group in " << path;
198  return false;
199 }
200 
201 QByteArray readTypeEntryForCurrentGroup(QFile &df, QByteArray *nextGroup, QByteArray *pName)
202 {
203  QByteArray group = *nextGroup;
204  QByteArray type;
205  if (group.isEmpty()) {
206  qCWarning(DESKTOPPARSER, "Read empty .desktop file group name! Invalid file?");
207  }
208  while (!df.atEnd()) {
209  QByteArray line = df.readLine().trimmed();
210  // skip empty lines and comments
211  if (line.isEmpty() || line.startsWith('#')) {
212  continue;
213  }
214  if (line.startsWith('[')) {
215  if (!line.endsWith(']')) {
216  qCWarning(DESKTOPPARSER) << "Illegal .desktop group definition (does not end with ']'):" << line;
217  }
218  QByteArray name = line.mid(1, line.lastIndexOf(']') - 1).trimmed();
219  // we have reached the next group -> return current group and Type= value
220  *nextGroup = name;
221  break;
222  }
223 
224  const static QRegularExpression typeEntryRegex(QStringLiteral("^Type\\s*=\\s*(.*)$"));
225  const auto match = typeEntryRegex.match(QString::fromUtf8(line));
226  if (match.hasMatch()) {
227  type = match.captured(1).toUtf8();
228  } else if (pName) {
229  const static QRegularExpression nameEntryRegex(QStringLiteral("^X-KDE-ServiceType\\s*=\\s*(.*)$"));
230  const auto nameMatch = nameEntryRegex.match(QString::fromUtf8(line));
231  if (nameMatch.hasMatch()) {
232  *pName = nameMatch.captured(1).toUtf8();
233  }
234  }
235  }
236  return type;
237 }
238 
239 bool tokenizeKeyValue(QFile &df, const QString &src, QByteArray &key, QString &value, int &lineNr)
240 {
241  const QByteArray line = df.readLine().trimmed();
242  lineNr++;
243  if (line.isEmpty()) {
244  DESKTOPTOJSON_VERBOSE_DEBUG << "Line " << lineNr << ": empty";
245  return true;
246  }
247  if (line.startsWith('#')) {
248  DESKTOPTOJSON_VERBOSE_DEBUG << "Line " << lineNr << ": comment";
249  return true; // skip comments
250  }
251  if (line.startsWith('[')) {
252  // start of new group -> doesn't interest us anymore
253  DESKTOPTOJSON_VERBOSE_DEBUG << "Line " << lineNr << ": start of new group " << line;
254  return false;
255  }
256  // must have form key=value now
257  const int equalsIndex = line.indexOf('=');
258  if (equalsIndex == -1) {
259  qCWarning(DESKTOPPARSER).nospace() << qPrintable(src) << ':' << lineNr
260  << ": Line is neither comment nor group "
261  "and doesn't contain an '=' character: \""
262  << line.constData() << '\"';
263  return true;
264  }
265  // trim key and value to remove spaces around the '=' char
266  key = line.mid(0, equalsIndex).trimmed();
267  if (key.isEmpty()) {
268  qCWarning(DESKTOPPARSER).nospace() << qPrintable(src) << ':' << lineNr << ": Key name is missing: \"" << line.constData() << '\"';
269  return true;
270  }
271 
272  const QByteArray valueRaw = line.mid(equalsIndex + 1).trimmed();
273  const QByteArray valueEscaped = escapeValue(valueRaw);
274  value = QString::fromUtf8(valueEscaped);
275 
276 #ifdef BUILDING_DESKTOPTOJSON_TOOL
277  DESKTOPTOJSON_VERBOSE_DEBUG.nospace() << "Line " << lineNr << ": key=" << key << ", value=" << value;
278  if (valueEscaped != valueRaw) {
279  DESKTOPTOJSON_VERBOSE_DEBUG << "Line " << lineNr << " contained escape sequences";
280  }
281 #endif
282 
283  return true;
284 }
285 
286 static QString locateRelativeServiceType(const QString &relPath)
287 {
288  return QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kservicetypes5/") + relPath);
289 }
290 
291 static ServiceTypeDefinition *parseServiceTypesFile(const QString &inputPath)
292 {
293  int lineNr = 0;
294  QString path = inputPath;
295  if (QDir::isRelativePath(path)) {
296  path = locateRelativeServiceType(path);
297  QString rcPath;
298  if (path.isEmpty()) {
299  rcPath = QLatin1String(":/kservicetypes5/") + inputPath;
300  if (QFileInfo::exists(rcPath)) {
301  path = rcPath;
302  }
303  }
304  if (path.isEmpty()) {
305  qCWarning(DESKTOPPARSER).nospace() << "Could not locate service type file kservicetypes5/" << qPrintable(inputPath) << ", tried "
307  return nullptr;
308  }
309  }
310  QFile df(path);
311  if (!df.exists()) {
312  qCCritical(DESKTOPPARSER) << "Service type file" << path << "does not exist";
313  return nullptr;
314  }
315  if (!readUntilDesktopEntryGroup(df, path, lineNr)) {
316  return nullptr;
317  }
318  ServiceTypeDefinition result;
319  // TODO: passing nextGroup by pointer is inefficient as it will make deep copies every time
320  // Not exactly performance critical code though so low priority
321  QByteArray nextGroup = "Desktop Entry";
322  // Type must be ServiceType now
323  QByteArray typeStr = readTypeEntryForCurrentGroup(df, &nextGroup, &result.m_serviceTypeName);
324  if (typeStr != QByteArrayLiteral("ServiceType")) {
325  qCWarning(DESKTOPPARSER) << path << "is not a valid service type: Type entry should be 'ServiceType', got" << typeStr << "instead.";
326  return nullptr;
327  }
328  while (!df.atEnd()) {
329  QByteArray currentGroup = nextGroup;
330  typeStr = readTypeEntryForCurrentGroup(df, &nextGroup, nullptr);
331  if (!currentGroup.startsWith(QByteArrayLiteral("PropertyDef::"))) {
332  qCWarning(DESKTOPPARSER) << "Skipping invalid group" << currentGroup << "in service type" << path;
333  continue;
334  }
335  if (typeStr.isEmpty()) {
336  qCWarning(DESKTOPPARSER) << "Could not find Type= key in group" << currentGroup;
337  continue;
338  }
339  QByteArray propertyName = currentGroup.mid(qstrlen("PropertyDef::"));
341  switch (type) {
342  case QVariant::String:
344  case QVariant::Int:
345  case QVariant::Double:
346  case QVariant::Bool:
347  qCDebug(DESKTOPPARSER) << "Found property definition" << propertyName << "with type" << typeStr;
348  result.m_propertyDefs.push_back(CustomPropertyDefinition(propertyName, type));
349  break;
350  case QVariant::Invalid:
351  qCWarning(DESKTOPPARSER) << "Property type" << typeStr
352  << "is not a known QVariant type."
353  " Found while parsing property definition for"
354  << propertyName << "in" << path;
355  break;
356  default:
357  qCWarning(DESKTOPPARSER) << "Unsupported property type" << typeStr << "for property" << propertyName << "found in" << path
358  << "\nOnly QString, QStringList, int, double and bool are supported.";
359  }
360  }
361  return new ServiceTypeDefinition(result);
362 }
363 
364 // a lazy map of service type definitions
365 typedef QCache<QString /*path*/, ServiceTypeDefinition> ServiceTypesHash;
366 Q_GLOBAL_STATIC(ServiceTypesHash, s_serviceTypes)
367 // access must be guarded by serviceTypesMutex as this code could be executed by multiple threads
368 QBasicMutex s_serviceTypesMutex;
369 } // end of anonymous namespace
370 
371 ServiceTypeDefinitions ServiceTypeDefinitions::fromFiles(const QStringList &paths)
372 {
373  ServiceTypeDefinitions ret;
374  ret.m_definitions.reserve(paths.size());
375  // as we might modify the cache we need to acquire a mutex here
376  for (const QString &serviceTypePath : paths) {
377  bool added = ret.addFile(serviceTypePath);
378  if (!added) {
379 #ifdef BUILDING_DESKTOPTOJSON_TOOL
380  exit(1); // this is a fatal error when using kcoreaddons_desktop_to_json()
381 #endif
382  }
383  }
384  return ret;
385 }
386 
387 bool ServiceTypeDefinitions::addFile(const QString &path)
388 {
389  QMutexLocker lock(&s_serviceTypesMutex);
390  ServiceTypeDefinition *def = s_serviceTypes->object(path);
391 
392  if (def) {
393  // in cache but we still must make our own copy
394  m_definitions << *def;
395  } else {
396  // not found in cache -> we need to parse the file
397  qCDebug(DESKTOPPARSER) << "About to parse service type file" << path;
398  def = parseServiceTypesFile(path);
399  if (!def) {
400  return false;
401  }
402 
403  m_definitions << *def; // This must *precede* insert call, insert might delete
404  s_serviceTypes->insert(path, def);
405  }
406  return true;
407 }
408 
409 QJsonValue ServiceTypeDefinitions::parseValue(const QByteArray &key, const QString &value) const
410 {
411  // check whether the key has a special type associated with it
412  for (const auto &def : m_definitions) {
413  for (const CustomPropertyDefinition &propertyDef : def.m_propertyDefs) {
414  if (propertyDef.key == key) {
415  return propertyDef.fromString(value);
416  }
417  }
418  }
419  qCDebug(DESKTOPPARSER) << "Unknown property type for key" << key << "-> falling back to string";
420  return QJsonValue(value);
421 }
422 
423 bool ServiceTypeDefinitions::hasServiceType(const QByteArray &serviceTypeName) const
424 {
425  const auto it = std::find_if(m_definitions.begin(), m_definitions.end(), [&serviceTypeName](const ServiceTypeDefinition &def) {
426  return def.m_serviceTypeName == serviceTypeName;
427  });
428  return it != m_definitions.end();
429 }
430 
431 void DesktopFileParser::convertToJson(const QByteArray &key,
432  ServiceTypeDefinitions &serviceTypes,
433  const QString &value,
434  QJsonObject &json,
435  QJsonObject &kplugin,
436  int lineNr)
437 {
438  /* The following keys are recognized (and added to a "KPlugin" object):
439 
440  Icon=mypluginicon
441  Type=Service
442  ServiceTypes=KPluginInfo
443  MimeType=text/plain;image/png
444 
445  Name=User Visible Name (translatable)
446  Comment=Description of what the plugin does (translatable)
447 
448  X-KDE-PluginInfo-Author=Author's Name
450  # alternatively to X-KDE-PluginInfo-Author & X-KDE-PluginInfo-Email:
451  X-KDE-PluginInfo-Authors=Author A's Name;AuthorB's Name (since KF 5.77)
452  [email protected];[email protected] (since KF 5.77)
453  X-KDE-PluginInfo-Name=internalname
454  X-KDE-PluginInfo-Version=1.1
455  X-KDE-PluginInfo-Website=http://www.plugin.org/
456  X-KDE-PluginInfo-Category=playlist
457  X-KDE-PluginInfo-License=GPL
458  X-KDE-PluginInfo-Copyright=Copyright <year> by Author's Name (since KF 5.77, translatable)
459  X-KDE-PluginInfo-EnabledByDefault=true
460  X-KDE-FormFactors=desktop
461  */
462  if (key == QByteArrayLiteral("Icon")) {
463  kplugin[QStringLiteral("Icon")] = value;
464  } else if (key == QByteArrayLiteral("X-KDE-PluginInfo-Name")) {
465  kplugin[QStringLiteral("Id")] = value;
466  } else if (key == QByteArrayLiteral("X-KDE-PluginInfo-Category")) {
467  kplugin[QStringLiteral("Category")] = value;
468  } else if (key == QByteArrayLiteral("X-KDE-PluginInfo-License")) {
469  kplugin[QStringLiteral("License")] = value;
470  } else if (key == QByteArrayLiteral("X-KDE-PluginInfo-Copyright")) {
471  kplugin[QStringLiteral("Copyright")] = value;
472  } else if (key.startsWith(QByteArrayLiteral("X-KDE-PluginInfo-Copyright["))) {
473  const QString languageSuffix = QString::fromUtf8(key.mid(qstrlen("X-KDE-PluginInfo-Copyright")));
474  kplugin[QStringLiteral("Copyright") + languageSuffix] = value;
475  } else if (key == QByteArrayLiteral("X-KDE-PluginInfo-Version")) {
476  kplugin[QStringLiteral("Version")] = value;
477  } else if (key == QByteArrayLiteral("X-KDE-PluginInfo-Website")) {
478  kplugin[QStringLiteral("Website")] = value;
479  }
480 #if KCOREADDONS_BUILD_DEPRECATED_SINCE(5, 79)
481  else if (key == QByteArrayLiteral("X-KDE-PluginInfo-Depends")) {
482  kplugin[QStringLiteral("Dependencies")] = QJsonArray::fromStringList(deserializeList(value));
483  qCDebug(DESKTOPPARSER) << "The X-KDE-PluginInfo-Depends property is deprecated and will be removed in KF6";
484  }
485 #endif
486  else if (key == QByteArrayLiteral("X-KDE-ServiceTypes") || key == QByteArrayLiteral("ServiceTypes")) {
487  // NOTE: "X-KDE-ServiceTypes" and "ServiceTypes" were already managed in the first parse step, so this second one is almost a noop
488  const auto services = deserializeList(value);
489  kplugin[QStringLiteral("ServiceTypes")] = QJsonArray::fromStringList(services);
490  } else if (key == QByteArrayLiteral("MimeType")) {
491  // MimeType is a XDG string list and not a KConfig list so we need to use ';' as the separator
492  kplugin[QStringLiteral("MimeTypes")] = QJsonArray::fromStringList(deserializeList(value, ';'));
493  // make sure that applications using kcoreaddons_desktop_to_json() that depend on reading
494  // the MimeType property still work (see https://git.reviewboard.kde.org/r/125527/)
495  json[QStringLiteral("MimeType")] = value; // TODO KF6 remove this compatibility code
496  } else if (key == QByteArrayLiteral("X-KDE-FormFactors")) {
497  kplugin[QStringLiteral("FormFactors")] = QJsonArray::fromStringList(deserializeList(value));
498  } else if (key == QByteArrayLiteral("X-KDE-PluginInfo-EnabledByDefault")) {
499  bool boolValue = false;
500  // should only be lower case, but be tolerant here
501  if (value.toLower() == QLatin1String("true")) {
502  boolValue = true;
503  } else {
504  if (value.toLower() != QLatin1String("false")) {
505  qCWarning(DESKTOPPARSER).nospace() << "Expected boolean value for key \"" << key << "\" at line " << lineNr << "but got \"" << value
506  << "\" instead.";
507  }
508  }
509  kplugin[QStringLiteral("EnabledByDefault")] = boolValue;
510  } else if (key == QByteArrayLiteral("X-KDE-PluginInfo-Author")) {
511  QJsonObject authorsObject = kplugin.value(QStringLiteral("Authors")).toArray().at(0).toObject();
512  // if the authors object doesn't exist yet this will create it
513  authorsObject[QStringLiteral("Name")] = value;
514  QJsonArray array;
515  array.append(authorsObject);
516  kplugin[QStringLiteral("Authors")] = array;
517  } else if (key == QByteArrayLiteral("X-KDE-PluginInfo-Email")) {
518  QJsonObject authorsObject = kplugin.value(QStringLiteral("Authors")).toArray().at(0).toObject();
519  // if the authors object doesn't exist yet this will create it
520  authorsObject[QStringLiteral("Email")] = value;
521  QJsonArray array;
522  array.append(authorsObject);
523  kplugin[QStringLiteral("Authors")] = array;
524  } else if (key == QByteArrayLiteral("X-KDE-PluginInfo-Authors")) {
525  const auto authors = deserializeList(value);
526  QJsonArray oldArray = kplugin.value(QStringLiteral("Authors")).toArray();
527  QJsonArray newArray;
528  for (int i = 0; i < authors.size(); ++i) {
529  QJsonObject authorsObject = oldArray.at(i).toObject();
530  // if the authors object doesn't exist yet this will create it
531  authorsObject[QStringLiteral("Name")] = authors[i];
532  newArray.append(authorsObject);
533  }
534  kplugin[QStringLiteral("Authors")] = newArray;
535  } else if (key == QByteArrayLiteral("X-KDE-PluginInfo-Emails")) {
536  const auto emails = deserializeList(value);
537  QJsonArray oldArray = kplugin.value(QStringLiteral("Authors")).toArray();
538  QJsonArray newArray;
539  for (int i = 0; i < emails.size(); ++i) {
540  QJsonObject authorsObject = oldArray.at(i).toObject();
541  // if the authors object doesn't exist yet this will create it
542  authorsObject[QStringLiteral("Email")] = emails[i];
543  newArray.append(authorsObject);
544  }
545  kplugin[QStringLiteral("Authors")] = newArray;
546  } else if (key == QByteArrayLiteral("Name") || key.startsWith(QByteArrayLiteral("Name["))) {
547  // TODO: also handle GenericName? does that make any sense, or is X-KDE-PluginInfo-Category enough?
548  kplugin[QString::fromUtf8(key)] = value;
549  } else if (key == QByteArrayLiteral("Comment")) {
550  kplugin[QStringLiteral("Description")] = value;
551  } else if (key.startsWith(QByteArrayLiteral("Comment["))) {
552  kplugin[QStringLiteral("Description") + QString::fromUtf8(key.mid(qstrlen("Comment")))] = value;
553  } else if (key == QByteArrayLiteral("InitialPreference")) {
554  kplugin[QStringLiteral("InitialPreference")] = value.toInt();
555  } else if (key == QByteArrayLiteral("Hidden")) {
556  DESKTOPTOJSON_VERBOSE_WARNING << "Hidden= key found in desktop file, this makes no sense"
557  " with metadata inside the plugin.";
558  kplugin[QString::fromUtf8(key)] = (value.toLower() == QLatin1String("true"));
559  } else if (key == QByteArrayLiteral("Exec") || key == QByteArrayLiteral("Type") || key == QByteArrayLiteral("Actions")
560  || key == QByteArrayLiteral("X-KDE-Library") || key == QByteArrayLiteral("Encoding")) {
561  // Exec= doesn't make sense here, however some .desktop files (like e.g. in kdevelop) have a dummy value here
562  // also the Type=Service entry is no longer needed
563  // Actions= is used as hack at least with the Dolphin KPart to report the different view mode options
564  // no plans yet to port that hack to JSON, so gently ignore it for now
565  // X-KDE-Library is also not needed since we already have the library to read this metadata
566  // Encoding= is also not converted as we always use utf-8 for reading
567  DESKTOPTOJSON_VERBOSE_DEBUG << "Not converting key " << key << "=" << value;
568  } else {
569  // check service type definitions or fall back to QString
570  json[QString::fromUtf8(key)] = serviceTypes.parseValue(key, value);
571  }
572 }
573 
574 bool DesktopFileParser::convert(const QString &src, const QStringList &serviceTypes, QJsonObject &json, QString *libraryPath)
575 {
576  QFile df(src);
577  int lineNr = 0;
578  ServiceTypeDefinitions serviceTypeDef = ServiceTypeDefinitions::fromFiles(serviceTypes);
579  readUntilDesktopEntryGroup(df, src, lineNr);
580  DESKTOPTOJSON_VERBOSE_DEBUG << "Found [Desktop Entry] group in line" << lineNr;
581  auto startPos = df.pos();
582 
583  // parse it a first time to know servicetype
584  while (!df.atEnd()) {
585  QByteArray key;
586  QString value;
587  if (!tokenizeKeyValue(df, src, key, value, lineNr)) {
588  break;
589  }
590  // some .desktop files still use the legacy ServiceTypes= key
591  if (key == QByteArrayLiteral("X-KDE-ServiceTypes") || key == QByteArrayLiteral("ServiceTypes")) {
592  const QString dotDesktop = QStringLiteral(".desktop");
593  const QChar slashChar(QLatin1Char('/'));
594  const auto serviceList = deserializeList(value);
595 
596  for (const auto &service : serviceList) {
597  if (!serviceTypeDef.hasServiceType(service.toLatin1())) {
598  // Make up the filename from the service type name. This assumes consistent naming...
599  QString absFileName = locateRelativeServiceType(service.toLower().replace(slashChar, QLatin1Char('-')) + dotDesktop);
600  if (absFileName.isEmpty()) {
601  absFileName = locateRelativeServiceType(service.toLower().remove(slashChar) + dotDesktop);
602  }
603  if (absFileName.isEmpty()) {
604  qCWarning(DESKTOPPARSER) << "Unable to find service type for service" << service << "listed in" << src;
605  } else {
606  serviceTypeDef.addFile(absFileName);
607  }
608  }
609  }
610  break;
611  }
612  }
613  lineNr = 0;
614  df.seek(startPos);
615 
616  QJsonObject kplugin; // the "KPlugin" key of the metadata
617  while (!df.atEnd()) {
618  QByteArray key;
619  QString value;
620  if (!tokenizeKeyValue(df, src, key, value, lineNr)) {
621  break;
622  } else if (key.isEmpty()) {
623  continue;
624  }
625 #ifdef BUILDING_DESKTOPTOJSON_TOOL
626  if (s_compatibilityMode) {
627  convertToCompatibilityJson(QString::fromUtf8(key), value, json, lineNr);
628  } else {
629  convertToJson(key, serviceTypeDef, value, json, kplugin, lineNr);
630  }
631 #else
632  convertToJson(key, serviceTypeDef, value, json, kplugin, lineNr);
633 #endif
634  if (libraryPath && key == QByteArrayLiteral("X-KDE-Library")) {
635  *libraryPath = value;
636  }
637  }
638  json[QStringLiteral("KPlugin")] = kplugin;
639  return true;
640 }
KDB_EXPORT QStringList deserializeList(const QString &data)
QString name(const QVariant &location)
QByteArray trimmed() const const
void reserve(int size)
int lastIndexOf(char ch, int from) const const
int size() const const
bool isEmpty() const const
bool startsWith(const QByteArray &ba) const const
int length() const const
bool exists() const const
double toDouble(bool *ok) const const
QStringList standardLocations(QStandardPaths::StandardLocation type)
void clear()
int indexOf(char ch, int from) const const
void append(const T &value)
QString fromUtf8(const char *str, int size)
QJsonValue at(int i) const const
QJsonObject toObject() const const
CaseInsensitive
QVariant::Type nameToType(const char *name)
int toInt(bool *ok, int base) const const
QJsonArray toArray() const const
bool isEmpty() const const
const char * constData() const const
QByteArray mid(int pos, int len) const const
virtual bool open(QIODevice::OpenMode mode) override
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
This is the main function which does scored fuzzy matching.
void append(const QJsonValue &value)
QByteArray & append(char ch)
void push_back(char ch)
QString toLower() const const
bool exists() const const
bool isRelativePath(const QString &path)
virtual bool atEnd() const const override
virtual bool seek(qint64 pos) override
int length() const const
void reserve(int size)
char * data()
QJsonValue value(const QString &key) const const
virtual qint64 pos() const const override
int size() const const
int compare(const QString &other, Qt::CaseSensitivity cs) const const
bool endsWith(const QByteArray &ba) const const
QString locate(QStandardPaths::StandardLocation type, const QString &fileName, QStandardPaths::LocateOptions options)
qint64 readLine(char *data, qint64 maxSize)
QJsonArray fromStringList(const QStringList &list)
This file is part of the KDE documentation.
Documentation copyright © 1996-2021 The KDE developers.
Generated on Thu Apr 15 2021 23:01:47 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.