KFileMetaData

appimageextractor.cpp
1 /*
2  SPDX-FileCopyrightText: 2019 Friedrich W. H. Kossebau <[email protected]>
3 
4  SPDX-License-Identifier: LGPL-2.1-or-later
5 */
6 
7 #include "appimageextractor.h"
8 
9 // KF
10 #include <KDesktopFile>
11 // Qt
12 #include <QTextDocument>
13 #include <QDomDocument>
14 #include <QTemporaryFile>
15 #include <QLocale>
16 // libappimage
17 #include <appimage/appimage.h>
18 
19 using namespace KFileMetaData;
20 
21 
22 namespace {
23 namespace AttributeNames {
24 QString xml_lang() { return QStringLiteral("xml:lang"); }
25 }
26 }
27 
28 
29 // helper class to extract the interesting data from the appdata file
30 // prefers localized strings over unlocalized, using system locale
31 class AppDataParser
32 {
33 public:
34  AppDataParser(const char* appImageFilePath, const QString& appdataFilePath);
35 
36 public:
37  QString summary() const { return !m_localized.summary.isEmpty() ? m_localized.summary : m_unlocalized.summary; }
38  QString description() const { return !m_localized.description.isEmpty() ? m_localized.description : m_unlocalized.description; }
39  QString developerName() const { return !m_localized.developerName.isEmpty() ? m_localized.developerName : m_unlocalized.developerName; }
40  QString projectLicense() const { return m_projectLicense; }
41 
42 private:
43  void extractDescription(const QDomElement& e, const QString& localeName);
44 
45 private:
46  struct Data {
47  QString summary;
48  QString description;
49  QString developerName;
50  };
51  Data m_localized;
52  Data m_unlocalized;
53  QString m_projectLicense;
54 };
55 
56 
57 AppDataParser::AppDataParser(const char* appImageFilePath, const QString& appdataFilePath)
58 {
59  if (appdataFilePath.isEmpty()) {
60  return;
61  }
62 
63  unsigned long size = 0L;
64  char* buf = nullptr;
65  bool ok = appimage_read_file_into_buffer_following_symlinks(appImageFilePath,
66  qUtf8Printable(appdataFilePath),
67  &buf,
68  &size);
69 
71 
72  if (!ok) {
73  return;
74  }
75 
76  QDomDocument domDocument;
77  if (!domDocument.setContent(QByteArray::fromRawData(buf, size))) {
78  return;
79  }
80 
81  QDomElement docElem = domDocument.documentElement();
82  if (docElem.tagName() != QLatin1String("component")) {
83  return;
84  }
85 
86  const auto localeName = QLocale::system().bcp47Name();
87 
88  QDomElement ec = docElem.firstChildElement();
89  while (!ec.isNull()) {
90  const auto tagName = ec.tagName();
91  const auto hasLangAttribute = ec.hasAttribute(AttributeNames::xml_lang());
92  const auto matchingLocale = hasLangAttribute && (ec.attribute(AttributeNames::xml_lang()) == localeName);
93  if (matchingLocale || !hasLangAttribute) {
94  if (tagName == QLatin1String("summary")) {
95  Data& data = hasLangAttribute ? m_localized : m_unlocalized;
96  data.summary = ec.text();
97  } else if (tagName == QLatin1String("description")) {
98  extractDescription(ec, localeName);
99  } else if (tagName == QLatin1String("developer_name")) {
100  Data& data = hasLangAttribute ? m_localized : m_unlocalized;
101  data.developerName = ec.text();
102  } else if (tagName == QLatin1String("project_license")) {
103  m_projectLicense = ec.text();
104  }
105  }
106  ec = ec.nextSiblingElement();
107  }
108 }
109 
110 using DesriptionDomFilter = std::function<bool(const QDomElement& e)>;
111 
112 void stripDescriptionTextElements(QDomElement& element, const DesriptionDomFilter& stripFilter)
113 {
114  auto childElement = element.firstChildElement();
115  while (!childElement.isNull()) {
116  auto nextChildElement = childElement.nextSiblingElement();
117 
118  const auto tagName = childElement.tagName();
119  const bool isElementToFilter = (tagName == QLatin1String("p")) || (tagName == QLatin1String("li"));
120  if (isElementToFilter && stripFilter(childElement)) {
121  element.removeChild(childElement);
122  } else {
123  stripDescriptionTextElements(childElement, stripFilter);
124  }
125 
126  childElement = nextChildElement;
127  }
128 }
129 
130 void AppDataParser::extractDescription(const QDomElement& e, const QString& localeName)
131 {
132  // create fake html from it and let QTextDocument transform it to plain text for us
133  QDomDocument descriptionDocument;
134  auto htmlElement = descriptionDocument.createElement(QStringLiteral("html"));
135  descriptionDocument.appendChild(htmlElement);
136 
137  // first localized...
138  auto clonedE = descriptionDocument.importNode(e, true).toElement();
139  clonedE.setTagName(QStringLiteral("body"));
140  stripDescriptionTextElements(clonedE, [localeName](const QDomElement& e) {
141  return !e.hasAttribute(AttributeNames::xml_lang()) ||
142  e.attribute(AttributeNames::xml_lang()) != localeName;
143  });
144  htmlElement.appendChild(clonedE);
145 
146  QTextDocument textDocument;
147  textDocument.setHtml(descriptionDocument.toString(-1));
148 
149  m_localized.description = textDocument.toPlainText().trimmed();
150 
151  if (!m_localized.description.isEmpty()) {
152  // localized will be preferred, no need to calculate unlocalized one
153  return;
154  }
155 
156  // then unlocalized if still needed
157  htmlElement.removeChild(clonedE); // reuse descriptionDocument
158  clonedE = descriptionDocument.importNode(e, true).toElement();
159  clonedE.setTagName(QStringLiteral("body"));
160  stripDescriptionTextElements(clonedE, [](const QDomElement& e) {
161  return e.hasAttribute(AttributeNames::xml_lang());
162  });
163  htmlElement.appendChild(clonedE);
164 
165  textDocument.setHtml(descriptionDocument.toString(-1));
166 
167  m_unlocalized.description = textDocument.toPlainText().trimmed();
168 }
169 
170 
171 // helper class to extract the interesting data from the desktop file
172 class DesktopFileParser
173 {
174 public:
175  DesktopFileParser(const char* appImageFilePath, const QString& desktopFilePath);
176 
177 public:
178  QString name;
179  QString comment;
180 };
181 
182 
183 DesktopFileParser::DesktopFileParser(const char* appImageFilePath, const QString& desktopFilePath)
184 {
185  if (desktopFilePath.isEmpty()) {
186  return;
187  }
188 
189  unsigned long size = 0L;
190  char* buf = nullptr;
191  bool ok = appimage_read_file_into_buffer_following_symlinks(appImageFilePath,
192  qUtf8Printable(desktopFilePath),
193  &buf,
194  &size);
195 
197 
198  if (!ok) {
199  return;
200  }
201 
202  // create real file, KDesktopFile needs that
203  QTemporaryFile tmpDesktopFile;
204  tmpDesktopFile.open();
205  tmpDesktopFile.write(buf, size);
206  tmpDesktopFile.close();
207 
208  KDesktopFile desktopFile(tmpDesktopFile.fileName());
209  name = desktopFile.readName();
210  comment = desktopFile.readComment();
211 }
212 
213 
214 AppImageExtractor::AppImageExtractor(QObject* parent)
215  : ExtractorPlugin(parent)
216 {
217 }
218 
219 QStringList AppImageExtractor::mimetypes() const
220 {
221  return QStringList{
222  QStringLiteral("application/x-iso9660-appimage"),
223  QStringLiteral("application/vnd.appimage"),
224  };
225 }
226 
227 void KFileMetaData::AppImageExtractor::extract(ExtractionResult* result)
228 {
229  const auto appImageFilePath = result->inputUrl().toUtf8();
230  const auto appImageType = appimage_get_type(appImageFilePath.constData(), false);
231  // not a valid appimage file?
232  if (appImageType <= 0) {
233  return;
234  }
235 
236  // find desktop file and appdata file
237  // need to scan ourselves, given there are no fixed names in the spec yet defined
238  // and we just can try as the other appimage tools to simply use the first file of the type found
239  char** filePaths = appimage_list_files(appImageFilePath.constData());
240  if (!filePaths) {
241  return;
242  }
243 
244  QString desktopFilePath;
245  QString appdataFilePath;
246  for (int i = 0; filePaths[i] != nullptr; ++i) {
247  const auto filePath = QString::fromUtf8(filePaths[i]);
248 
249  if (filePath.startsWith(QLatin1String("usr/share/metainfo/")) &&
250  filePath.endsWith(QLatin1String(".appdata.xml"))) {
251  appdataFilePath = filePath;
252  if (!desktopFilePath.isEmpty()) {
253  break;
254  }
255  }
256 
257  if (filePath.endsWith(QLatin1String(".desktop")) && !filePath.contains(QLatin1Char('/'))) {
258  desktopFilePath = filePath;
259  if (!appdataFilePath.isEmpty()) {
260  break;
261  }
262  }
263  }
264 
265  appimage_string_list_free(filePaths);
266 
267  // extract data from both files...
268  const AppDataParser appData(appImageFilePath.constData(), appdataFilePath);
269 
270  const DesktopFileParser desktopFileData(appImageFilePath.constData(), desktopFilePath);
271 
272  // ... and insert into the result
273  result->add(Property::Title, desktopFileData.name);
274 
275  if (!desktopFileData.comment.isEmpty()) {
276  result->add(Property::Comment, desktopFileData.comment);
277  } else if (!appData.summary().isEmpty()) {
278  result->add(Property::Comment, appData.summary());
279  }
280  if (!appData.description().isEmpty()) {
281  result->add(Property::Description, appData.description());
282  }
283  if (!appData.projectLicense().isEmpty()) {
284  result->add(Property::License, appData.projectLicense());
285  }
286  if (!appData.developerName().isEmpty()) {
287  result->add(Property::Author, appData.developerName());
288  }
289 }
QString name(const QVariant &location)
QDomNode appendChild(const QDomNode &newChild)
QString attribute(const QString &name, const QString &defValue) const const
void setTagName(const QString &name)
QString toString(int indent) const const
virtual void add(Property::Property property, const QVariant &value)=0
This function is called by the plugins when they wish to add a key value pair which should be indexed...
The ExtractorPlugin is the base class for all file metadata extractors.
QDomElement nextSiblingElement(const QString &tagName) const const
QByteArray fromRawData(const char *data, int size)
QDomElement documentElement() const const
QLocale system()
QDomNode importNode(const QDomNode &importedNode, bool deep)
QDomElement toElement() const const
QString fromUtf8(const char *str, int size)
QString text() const const
bool hasAttribute(const QString &name) const const
bool isEmpty() const const
QString trimmed() const const
QDomNode removeChild(const QDomNode &oldChild)
bool isNull() const const
virtual QString fileName() const const override
QString toPlainText() const const
virtual void close() override
QDomElement firstChildElement(const QString &tagName) const const
QString bcp47Name() const const
void setHtml(const QString &html)
qint64 write(const char *data, qint64 maxSize)
QString tagName() const const
QDomElement createElement(const QString &tagName)
The ExtractionResult class is where all the data extracted by the indexer is saved.
QString inputUrl() const
The input url which the plugins will use to locate the file.
bool setContent(const QByteArray &data, bool namespaceProcessing, QString *errorMsg, int *errorLine, int *errorColumn)
QByteArray toUtf8() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2021 The KDE developers.
Generated on Sat Jan 23 2021 03:15:05 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.