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

KDE's Doxygen guidelines are available online.