File indexing completed on 2025-03-16 12:49:35
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"