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 toString(int indent) const const
QString text() const const
QDomElement toElement() const const
The ExtractionResult class is where all the data extracted by the indexer is saved....
QString fromUtf8(const char *str, int size)
QDomNode importNode(const QDomNode &importedNode, bool deep)
QByteArray fromRawData(const char *data, int size)
QString tagName() const const
QDomNode removeChild(const QDomNode &oldChild)
bool isNull() const const
QString trimmed() const const
QString bcp47Name() const const
bool setContent(const QByteArray &data, bool namespaceProcessing, QString *errorMsg, int *errorLine, int *errorColumn)
QDomElement createElement(const QString &tagName)
QString inputUrl() const
The input url which the plugins will use to locate the file.
QString toPlainText() const const
virtual QString fileName() const const override
void setTagName(const QString &name)
QLocale system()
bool isEmpty() const const
QByteArray toUtf8() const const
QDomElement nextSiblingElement(const QString &tagName) const const
virtual void close() override
QDomElement documentElement() const const
QDomElement firstChildElement(const QString &tagName) const const
bool hasAttribute(const QString &name) const const
QDomNode appendChild(const QDomNode &newChild)
void setHtml(const QString &html)
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...
const char * name(StandardAction id)
The ExtractorPlugin is the base class for all file metadata extractors. It is responsible for extract...
QString attribute(const QString &name, const QString &defValue) const const
qint64 write(const char *data, qint64 maxSize)
This file is part of the KDE documentation.
Documentation copyright © 1996-2022 The KDE developers.
Generated on Thu May 26 2022 03:46:07 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.