KFileMetaData

appimageextractor.cpp
1/*
2 SPDX-FileCopyrightText: 2019 Friedrich W. H. Kossebau <kossebau@kde.org>
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
19using namespace KFileMetaData;
20
21
22namespace {
23namespace AttributeNames {
24QString 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
31class AppDataParser
32{
33public:
34 AppDataParser(const char* appImageFilePath, const QString& appdataFilePath);
35
36public:
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
42private:
43 void extractDescription(const QDomElement& e, const QString& localeName);
44
45private:
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
57AppDataParser::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
110using DesriptionDomFilter = std::function<bool(const QDomElement& e)>;
111
112void 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
130void 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
172class DesktopFileParser
173{
174public:
175 DesktopFileParser(const char* appImageFilePath, const QString& desktopFilePath);
176
177public:
178 QString name;
179 QString comment;
180};
181
182
183DesktopFileParser::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
214AppImageExtractor::AppImageExtractor(QObject* parent)
215 : ExtractorPlugin(parent)
216{
217}
218
219QStringList AppImageExtractor::mimetypes() const
220{
221 return QStringList{
222 QStringLiteral("application/x-iso9660-appimage"),
223 QStringLiteral("application/vnd.appimage"),
224 };
225}
226
227void 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}
290
291#include "moc_appimageextractor.cpp"
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.
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.
QByteArray fromRawData(const char *data, qsizetype size)
QDomElement createElement(const QString &tagName)
QDomElement documentElement() const const
QDomNode importNode(const QDomNode &importedNode, bool deep)
ParseResult setContent(QAnyStringView text, ParseOptions options)
QString toString(int indent) const const
QString attribute(const QString &name, const QString &defValue) const const
bool hasAttribute(const QString &name) const const
void setTagName(const QString &name)
QString tagName() const const
QString text() const const
QDomNode appendChild(const QDomNode &newChild)
QDomElement firstChildElement(const QString &tagName, const QString &namespaceURI) const const
bool isNull() const const
QDomElement nextSiblingElement(const QString &tagName, const QString &namespaceURI) const const
QDomNode removeChild(const QDomNode &oldChild)
QDomElement toElement() const const
virtual void close() override
qint64 write(const QByteArray &data)
QString bcp47Name() const const
QLocale system()
QString fromUtf8(QByteArrayView str)
bool isEmpty() const const
QByteArray toUtf8() const const
QString trimmed() const const
virtual QString fileName() const const override
void setHtml(const QString &html)
QString toPlainText() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:17:53 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.