KFileMetaData

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

KDE's Doxygen guidelines are available online.