diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,6 +40,11 @@ URL "https://download.kde.org/stable/frameworks/" TYPE OPTIONAL PURPOSE "Archive is needed to build ODF and OOXML 2007 extractors") +find_package(KF5 ${KF5_DEP_VERSION} COMPONENTS Config) +set_package_properties(KF5Config PROPERTIES DESCRIPTION "KDE Frameworks 5: Config Framework" + URL "https://download.kde.org/stable/frameworks/" TYPE OPTIONAL + PURPOSE "Config is needed to build the AppImage extractor") + find_package(KF5 ${KF5_DEP_VERSION} REQUIRED COMPONENTS I18n) find_package(Poppler 0.12.1 COMPONENTS Qt5) @@ -92,6 +97,19 @@ # URL "https://projects.kde.org/projects/kde/kdegraphics/kdegraphics-mobipocket" # TYPE OPTIONAL PURPOSE "Support for mobi metadata") +find_package(libappimage) +set_package_properties(libappimage PROPERTIES DESCRIPTION "Core library of the AppImage project" + URL "https://github.com/AppImage/libappimage" + TYPE OPTIONAL + PURPOSE "Provides support for AppImage thumbnails" + ) +if (libappimage_FOUND) + # workaround for currently released libappimage versions (sadly no version check possible yet) + if (NOT LIBAPPIMAGE_INCLUDE_DIRS) + get_target_property(LIBAPPIMAGE_INCLUDE_DIRS libappimage INTERFACE_INCLUDE_DIRECTORIES) + endif() +endif() + add_definitions(-DTRANSLATION_DOMAIN=\"kfilemetadata5\") add_subdirectory(src) diff --git a/src/extractors/CMakeLists.txt b/src/extractors/CMakeLists.txt --- a/src/extractors/CMakeLists.txt +++ b/src/extractors/CMakeLists.txt @@ -214,3 +214,19 @@ DESTINATION ${PLUGIN_INSTALL_DIR}/kf5/kfilemetadata) endif() + +if(libappimage_FOUND AND KF5Config_FOUND) + add_library(kfilemetadata_appimageextractor MODULE appimageextractor.cpp ) + target_include_directories(kfilemetadata_appimageextractor SYSTEM PRIVATE ${LIBAPPIMAGE_INCLUDE_DIRS}) + target_link_libraries( kfilemetadata_appimageextractor + KF5::FileMetaData + KF5::ConfigCore + Qt5::Xml + ${LIBAPPIMAGE_LIBRARIES} + ) + + set_target_properties(kfilemetadata_appimageextractor PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/kf5/kfilemetadata") + install( + TARGETS kfilemetadata_appimageextractor + DESTINATION ${PLUGIN_INSTALL_DIR}/kf5/kfilemetadata) +endif() diff --git a/src/extractors/appimageextractor.h b/src/extractors/appimageextractor.h new file mode 100644 --- /dev/null +++ b/src/extractors/appimageextractor.h @@ -0,0 +1,43 @@ +/* + Copyright (C) 2019 Friedrich W. H. Kossebau + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef APPIMAGEEXTRACTOR_H +#define APPIMAGEEXTRACTOR_H + +#include "extractorplugin.h" + +namespace KFileMetaData +{ + +class AppImageExtractor : public ExtractorPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.kde.kf5.kfilemetadata.ExtractorPlugin") + Q_INTERFACES(KFileMetaData::ExtractorPlugin) + +public: + explicit AppImageExtractor(QObject* parent = nullptr); + +public: + void extract(ExtractionResult* result) override; + QStringList mimetypes() const override; +}; + +} + +#endif diff --git a/src/extractors/appimageextractor.cpp b/src/extractors/appimageextractor.cpp new file mode 100644 --- /dev/null +++ b/src/extractors/appimageextractor.cpp @@ -0,0 +1,212 @@ +/* + Copyright (C) 2019 Friedrich W. H. Kossebau + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "appimageextractor.h" + +// KF +#include +// Qt +#include +#include +#include +// libappimage +#include + +using namespace KFileMetaData; + + +// helper class to extract the interesting data from the appdata file +class AppDataParser +{ +public: + AppDataParser(const char* appImageFilePath, const QString& appdataFilePath); + +public: + QString name; + QString summary; + QString project_license; + QString developer_name; +}; + + +AppDataParser::AppDataParser(const char* appImageFilePath, const QString& appdataFilePath) +{ + if (appdataFilePath.isEmpty()) { + return; + } + + unsigned long size = 0L; + char* buf = nullptr; + bool ok = appimage_read_file_into_buffer_following_symlinks(appImageFilePath, + qUtf8Printable(appdataFilePath), + &buf, + &size); + + QScopedPointer cleanup(buf); + + if (!ok) { + return; + } + + QDomDocument domDocument; + if (!domDocument.setContent(QByteArray::fromRawData(buf, size))) { + return; + } + + QDomElement docElem = domDocument.documentElement(); + if (docElem.tagName() != QLatin1String("component")) { + return; + } + + QDomElement ec = docElem.firstChildElement(); + while (!ec.isNull()) { + const auto tagName = ec.tagName(); + // we do not know for which locale display/processing the filemetadata is fetched, + // so for now use the untranslated one + if (!ec.hasAttribute(QLatin1String("xml:lang"))) { + if (tagName == QLatin1String("name")) { + name = ec.text(); + } else if (tagName == QLatin1String("summary")) { + summary = ec.text(); + } else if (tagName == QLatin1String("project_license")) { + project_license = ec.text(); + } else if (tagName == QLatin1String("developer_name")) { + developer_name = ec.text(); + } + } + ec = ec.nextSiblingElement(); + } +} + + +// helper class to extract the interesting data from the desktop file +class DesktopFileParser +{ +public: + DesktopFileParser(const char* appImageFilePath, const QString& desktopFilePath); + +public: + QString name; + QString comment; +}; + + +DesktopFileParser::DesktopFileParser(const char* appImageFilePath, const QString& desktopFilePath) +{ + if (desktopFilePath.isEmpty()) { + return; + } + + unsigned long size = 0L; + char* buf = nullptr; + bool ok = appimage_read_file_into_buffer_following_symlinks(appImageFilePath, + qUtf8Printable(desktopFilePath), + &buf, + &size); + + QScopedPointer cleanup(buf); + + if (!ok) { + return; + } + + // create real file, KDesktopFile needs that + QTemporaryFile tmpDesktopFile; + tmpDesktopFile.open(); + tmpDesktopFile.write(buf, size); + tmpDesktopFile.close(); + + KDesktopFile desktopFile(tmpDesktopFile.fileName()); + name = desktopFile.readName(); + comment = desktopFile.readComment(); +} + + +AppImageExtractor::AppImageExtractor(QObject* parent) + : ExtractorPlugin(parent) +{ +} + +QStringList AppImageExtractor::mimetypes() const +{ + return QStringList{ + QStringLiteral("application/x-iso9660-appimage"), + QStringLiteral("application/vnd.appimage"), + }; +} + +void KFileMetaData::AppImageExtractor::extract(ExtractionResult* result) +{ + const auto appImageFilePath = result->inputUrl().toUtf8(); + const auto appImageType = appimage_get_type(appImageFilePath.constData(), false); + // not a valid appimage file? + if (appImageType <= 0) { + return; + } + + // find desktop file and appdata file + // need to scan ourselves, given there are no fixed names in the spec yet defined + // and we just can try as the other appimage tools to simply use the first file of the type found + char** filePaths = appimage_list_files(appImageFilePath.constData()); + if (!filePaths) { + return; + } + + QString desktopFilePath; + QString appdataFilePath; + for (int i = 0; filePaths[i] != nullptr; ++i) { + const auto filePath = QString::fromUtf8(filePaths[i]); + + if (filePath.startsWith(QLatin1String("usr/share/metainfo/")) && + filePath.endsWith(QLatin1String(".appdata.xml"))) { + appdataFilePath = filePath; + if (!desktopFilePath.isEmpty()) { + break; + } + } + + if (filePath.endsWith(QLatin1String(".desktop")) && !filePath.contains(QLatin1Char('/'))) { + desktopFilePath = filePath; + if (!appdataFilePath.isEmpty()) { + break; + } + } + } + + appimage_string_list_free(filePaths); + + // extract data from both files... + const AppDataParser appData(appImageFilePath.constData(), appdataFilePath); + + const DesktopFileParser desktopFileData(appImageFilePath.constData(), desktopFilePath); + + // ... and insert into the result + result->add(Property::Title, desktopFileData.name); + + if (!desktopFileData.comment.isEmpty()) { + result->add(Property::Comment, desktopFileData.comment); + } else if (!appData.summary.isEmpty()) { + result->add(Property::Comment, appData.summary); + } + if (!appData.project_license.isEmpty()) { + result->add(Property::License, appData.project_license); + } + if (!appData.developer_name.isEmpty()) { + result->add(Property::Author, appData.developer_name); + } +}