File indexing completed on 2024-05-12 15:37:04

0001 /*
0002     SPDX-FileCopyrightText: 2019 Friedrich W. H. Kossebau <kossebau@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.1-or-later
0005 */
0006 
0007 #include "appimageextractor.h"
0008 
0009 // KF
0010 #include <KDesktopFile>
0011 // Qt
0012 #include <QTextDocument>
0013 #include <QDomDocument>
0014 #include <QTemporaryFile>
0015 #include <QLocale>
0016 // libappimage
0017 #include <appimage/appimage.h>
0018 
0019 using namespace KFileMetaData;
0020 
0021 
0022 namespace {
0023 namespace AttributeNames {
0024 QString xml_lang() { return QStringLiteral("xml:lang"); }
0025 }
0026 }
0027 
0028 
0029 // helper class to extract the interesting data from the appdata file
0030 // prefers localized strings over unlocalized, using system locale
0031 class AppDataParser
0032 {
0033 public:
0034     AppDataParser(const char* appImageFilePath, const QString& appdataFilePath);
0035 
0036 public:
0037     QString summary() const        { return !m_localized.summary.isEmpty() ? m_localized.summary : m_unlocalized.summary; }
0038     QString description() const    { return !m_localized.description.isEmpty() ? m_localized.description : m_unlocalized.description; }
0039     QString developerName() const  { return !m_localized.developerName.isEmpty() ? m_localized.developerName : m_unlocalized.developerName; }
0040     QString projectLicense() const { return m_projectLicense; }
0041 
0042 private:
0043     void extractDescription(const QDomElement& e, const QString& localeName);
0044 
0045 private:
0046     struct Data {
0047         QString summary;
0048         QString description;
0049         QString developerName;
0050     };
0051     Data m_localized;
0052     Data m_unlocalized;
0053     QString m_projectLicense;
0054 };
0055 
0056 
0057 AppDataParser::AppDataParser(const char* appImageFilePath, const QString& appdataFilePath)
0058 {
0059     if (appdataFilePath.isEmpty()) {
0060         return;
0061     }
0062 
0063     unsigned long size = 0L;
0064     char* buf = nullptr;
0065     bool ok = appimage_read_file_into_buffer_following_symlinks(appImageFilePath,
0066                                                                 qUtf8Printable(appdataFilePath),
0067                                                                 &buf,
0068                                                                 &size);
0069 
0070     QScopedPointer<char, QScopedPointerPodDeleter> cleanup(buf);
0071 
0072     if (!ok) {
0073         return;
0074     }
0075 
0076     QDomDocument domDocument;
0077     if (!domDocument.setContent(QByteArray::fromRawData(buf, size))) {
0078         return;
0079     }
0080 
0081     QDomElement docElem = domDocument.documentElement();
0082     if (docElem.tagName() != QLatin1String("component")) {
0083         return;
0084     }
0085 
0086     const auto localeName = QLocale::system().bcp47Name();
0087 
0088     QDomElement ec = docElem.firstChildElement();
0089     while (!ec.isNull()) {
0090         const auto tagName = ec.tagName();
0091         const auto hasLangAttribute = ec.hasAttribute(AttributeNames::xml_lang());
0092         const auto matchingLocale = hasLangAttribute && (ec.attribute(AttributeNames::xml_lang()) == localeName);
0093         if (matchingLocale || !hasLangAttribute) {
0094             if (tagName == QLatin1String("summary")) {
0095                 Data& data = hasLangAttribute ? m_localized : m_unlocalized;
0096                 data.summary = ec.text();
0097             } else if (tagName == QLatin1String("description")) {
0098                 extractDescription(ec, localeName);
0099             } else if (tagName == QLatin1String("developer_name")) {
0100                 Data& data = hasLangAttribute ? m_localized : m_unlocalized;
0101                 data.developerName = ec.text();
0102             } else if (tagName == QLatin1String("project_license")) {
0103                 m_projectLicense = ec.text();
0104             }
0105         }
0106         ec = ec.nextSiblingElement();
0107     }
0108 }
0109 
0110 using DesriptionDomFilter = std::function<bool(const QDomElement& e)>;
0111 
0112 void stripDescriptionTextElements(QDomElement& element, const DesriptionDomFilter& stripFilter)
0113 {
0114     auto childElement = element.firstChildElement();
0115     while (!childElement.isNull()) {
0116         auto nextChildElement = childElement.nextSiblingElement();
0117 
0118         const auto tagName = childElement.tagName();
0119         const bool isElementToFilter = (tagName == QLatin1String("p")) || (tagName == QLatin1String("li"));
0120         if (isElementToFilter && stripFilter(childElement)) {
0121             element.removeChild(childElement);
0122         } else {
0123             stripDescriptionTextElements(childElement, stripFilter);
0124         }
0125 
0126         childElement = nextChildElement;
0127     }
0128 }
0129 
0130 void AppDataParser::extractDescription(const QDomElement& e, const QString& localeName)
0131 {
0132     // create fake html from it and let QTextDocument transform it to plain text for us
0133     QDomDocument descriptionDocument;
0134     auto htmlElement = descriptionDocument.createElement(QStringLiteral("html"));
0135     descriptionDocument.appendChild(htmlElement);
0136 
0137     // first localized...
0138     auto clonedE = descriptionDocument.importNode(e, true).toElement();
0139     clonedE.setTagName(QStringLiteral("body"));
0140     stripDescriptionTextElements(clonedE, [localeName](const QDomElement& e) {
0141         return !e.hasAttribute(AttributeNames::xml_lang()) ||
0142             e.attribute(AttributeNames::xml_lang()) != localeName;
0143     });
0144     htmlElement.appendChild(clonedE);
0145 
0146     QTextDocument textDocument;
0147     textDocument.setHtml(descriptionDocument.toString(-1));
0148 
0149     m_localized.description = textDocument.toPlainText().trimmed();
0150 
0151     if (!m_localized.description.isEmpty()) {
0152         // localized will be preferred, no need to calculate unlocalized one
0153         return;
0154     }
0155 
0156     // then unlocalized if still needed
0157     htmlElement.removeChild(clonedE); // reuse descriptionDocument
0158     clonedE = descriptionDocument.importNode(e, true).toElement();
0159     clonedE.setTagName(QStringLiteral("body"));
0160     stripDescriptionTextElements(clonedE, [](const QDomElement& e) {
0161         return e.hasAttribute(AttributeNames::xml_lang());
0162     });
0163     htmlElement.appendChild(clonedE);
0164 
0165     textDocument.setHtml(descriptionDocument.toString(-1));
0166 
0167     m_unlocalized.description = textDocument.toPlainText().trimmed();
0168 }
0169 
0170 
0171 // helper class to extract the interesting data from the desktop file
0172 class DesktopFileParser
0173 {
0174 public:
0175     DesktopFileParser(const char* appImageFilePath, const QString& desktopFilePath);
0176 
0177 public:
0178     QString name;
0179     QString comment;
0180 };
0181 
0182 
0183 DesktopFileParser::DesktopFileParser(const char* appImageFilePath, const QString& desktopFilePath)
0184 {
0185     if (desktopFilePath.isEmpty()) {
0186         return;
0187     }
0188 
0189     unsigned long size = 0L;
0190     char* buf = nullptr;
0191     bool ok = appimage_read_file_into_buffer_following_symlinks(appImageFilePath,
0192                                                                 qUtf8Printable(desktopFilePath),
0193                                                                 &buf,
0194                                                                 &size);
0195 
0196     QScopedPointer<char, QScopedPointerPodDeleter> cleanup(buf);
0197 
0198     if (!ok) {
0199         return;
0200     }
0201 
0202     // create real file, KDesktopFile needs that
0203     QTemporaryFile tmpDesktopFile;
0204     tmpDesktopFile.open();
0205     tmpDesktopFile.write(buf, size);
0206     tmpDesktopFile.close();
0207 
0208     KDesktopFile desktopFile(tmpDesktopFile.fileName());
0209     name = desktopFile.readName();
0210     comment = desktopFile.readComment();
0211 }
0212 
0213 
0214 AppImageExtractor::AppImageExtractor(QObject* parent)
0215     : ExtractorPlugin(parent)
0216 {
0217 }
0218 
0219 QStringList AppImageExtractor::mimetypes() const
0220 {
0221     return QStringList{
0222         QStringLiteral("application/x-iso9660-appimage"),
0223         QStringLiteral("application/vnd.appimage"),
0224     };
0225 }
0226 
0227 void KFileMetaData::AppImageExtractor::extract(ExtractionResult* result)
0228 {
0229     const auto appImageFilePath = result->inputUrl().toUtf8();
0230     const auto appImageType = appimage_get_type(appImageFilePath.constData(), false);
0231     // not a valid appimage file?
0232     if (appImageType <= 0) {
0233         return;
0234     }
0235 
0236     // find desktop file and appdata file
0237     // need to scan ourselves, given there are no fixed names in the spec yet defined
0238     // and we just can try as the other appimage tools to simply use the first file of the type found
0239     char** filePaths = appimage_list_files(appImageFilePath.constData());
0240     if (!filePaths) {
0241         return;
0242     }
0243 
0244     QString desktopFilePath;
0245     QString appdataFilePath;
0246     for (int i = 0; filePaths[i] != nullptr; ++i) {
0247         const auto filePath = QString::fromUtf8(filePaths[i]);
0248 
0249         if (filePath.startsWith(QLatin1String("usr/share/metainfo/")) &&
0250             filePath.endsWith(QLatin1String(".appdata.xml"))) {
0251             appdataFilePath = filePath;
0252             if (!desktopFilePath.isEmpty()) {
0253                 break;
0254             }
0255         }
0256 
0257         if (filePath.endsWith(QLatin1String(".desktop")) && !filePath.contains(QLatin1Char('/'))) {
0258             desktopFilePath = filePath;
0259             if (!appdataFilePath.isEmpty()) {
0260                 break;
0261             }
0262         }
0263     }
0264 
0265     appimage_string_list_free(filePaths);
0266 
0267     // extract data from both files...
0268     const AppDataParser appData(appImageFilePath.constData(), appdataFilePath);
0269 
0270     const DesktopFileParser desktopFileData(appImageFilePath.constData(), desktopFilePath);
0271 
0272     // ... and insert into the result
0273     result->add(Property::Title, desktopFileData.name);
0274 
0275     if (!desktopFileData.comment.isEmpty()) {
0276         result->add(Property::Comment, desktopFileData.comment);
0277     } else if (!appData.summary().isEmpty()) {
0278         result->add(Property::Comment, appData.summary());
0279     }
0280     if (!appData.description().isEmpty()) {
0281         result->add(Property::Description, appData.description());
0282     }
0283     if (!appData.projectLicense().isEmpty()) {
0284         result->add(Property::License, appData.projectLicense());
0285     }
0286     if (!appData.developerName().isEmpty()) {
0287         result->add(Property::Author, appData.developerName());
0288     }
0289 }
0290 
0291 #include "moc_appimageextractor.cpp"