Import libappimage_1.0.4-5.orig.tar.gz
authorPino Toscano <pino@debian.org>
Mon, 2 Jan 2023 07:20:13 +0000 (08:20 +0100)
committerPino Toscano <pino@debian.org>
Mon, 2 Jan 2023 07:20:13 +0000 (08:20 +0100)
[dgit import orig libappimage_1.0.4-5.orig.tar.gz]

166 files changed:
.github/workflows/main.yml [new file with mode: 0644]
.gitignore [new file with mode: 0644]
.gitmodules [new file with mode: 0644]
CMakeLists.txt [new file with mode: 0644]
LICENSE [new file with mode: 0644]
README.md [new file with mode: 0644]
ci/Dockerfile.bionic-i386 [new file with mode: 0644]
ci/Dockerfile.bionic-x86_64 [new file with mode: 0644]
ci/Dockerfile.xenial-i386 [new file with mode: 0644]
ci/Dockerfile.xenial-x86_64 [new file with mode: 0644]
ci/build-and-test.sh [new file with mode: 0755]
ci/build-docker-image.sh [new file with mode: 0755]
ci/build-in-docker.sh [new file with mode: 0755]
ci/install-deps.sh [new file with mode: 0755]
cmake/CodeCoverage.cmake [new file with mode: 0644]
cmake/Modules/Findsquashfuse.cmake [new file with mode: 0644]
cmake/dependencies.cmake [new file with mode: 0644]
cmake/imported_dependencies.cmake.in [new file with mode: 0644]
cmake/libappimage.pc.in [new file with mode: 0644]
cmake/libappimageConfig.cmake.in [new file with mode: 0644]
cmake/libappimageConfigVersion.cmake.in [new file with mode: 0644]
cmake/reproducible_builds.cmake [new file with mode: 0644]
cmake/scripts.cmake [new file with mode: 0644]
cmake/tools.cmake [new file with mode: 0644]
docs/.gitignore [new file with mode: 0644]
docs/Makefile [new file with mode: 0644]
docs/conf.py [new file with mode: 0644]
docs/index.rst [new file with mode: 0644]
docs/make.bat [new file with mode: 0644]
docs/make.sh [new file with mode: 0755]
docs/requirements.txt [new file with mode: 0644]
include/appimage/appimage++.h [new file with mode: 0644]
include/appimage/appimage.h [new file with mode: 0644]
include/appimage/appimage_legacy.h [new file with mode: 0644]
include/appimage/appimage_shared.h [new file with mode: 0644]
include/appimage/core/AppImage.h [new file with mode: 0644]
include/appimage/core/AppImageFormat.h [new file with mode: 0644]
include/appimage/core/PayloadEntryType.h [new file with mode: 0644]
include/appimage/core/PayloadIterator.h [new file with mode: 0644]
include/appimage/core/exceptions.h [new file with mode: 0644]
include/appimage/desktop_integration/IntegrationManager.h [new file with mode: 0644]
include/appimage/desktop_integration/exceptions.h [new file with mode: 0644]
include/appimage/utils/ResourcesExtractor.h [new file with mode: 0644]
include/appimage/utils/logging.h [new file with mode: 0644]
lib/CMakeLists.txt [new file with mode: 0644]
src/CMakeLists.txt [new file with mode: 0644]
src/libappimage/CMakeLists.txt [new file with mode: 0644]
src/libappimage/appimage_handler.h [new file with mode: 0644]
src/libappimage/config.h.in [new file with mode: 0644]
src/libappimage/core/AppImage.cpp [new file with mode: 0644]
src/libappimage/core/CMakeLists.txt [new file with mode: 0644]
src/libappimage/core/PayloadIterator.cpp [new file with mode: 0644]
src/libappimage/core/Traversal.cpp [new file with mode: 0644]
src/libappimage/core/Traversal.h [new file with mode: 0644]
src/libappimage/core/impl/PayloadIStream.h [new file with mode: 0644]
src/libappimage/core/impl/StreambufType1.cpp [new file with mode: 0644]
src/libappimage/core/impl/StreambufType1.h [new file with mode: 0644]
src/libappimage/core/impl/StreambufType2.cpp [new file with mode: 0644]
src/libappimage/core/impl/StreambufType2.h [new file with mode: 0644]
src/libappimage/core/impl/TraversalType1.cpp [new file with mode: 0644]
src/libappimage/core/impl/TraversalType1.h [new file with mode: 0644]
src/libappimage/core/impl/TraversalType2.cpp [new file with mode: 0644]
src/libappimage/core/impl/TraversalType2.h [new file with mode: 0644]
src/libappimage/desktop_integration/CMakeLists.txt [new file with mode: 0644]
src/libappimage/desktop_integration/IntegrationManager.cpp [new file with mode: 0644]
src/libappimage/desktop_integration/Thumbnailer.cpp [new file with mode: 0644]
src/libappimage/desktop_integration/Thumbnailer.h [new file with mode: 0644]
src/libappimage/desktop_integration/constants.h [new file with mode: 0644]
src/libappimage/desktop_integration/integrator/DesktopEntryEditError.h [new file with mode: 0644]
src/libappimage/desktop_integration/integrator/DesktopEntryEditor.cpp [new file with mode: 0644]
src/libappimage/desktop_integration/integrator/DesktopEntryEditor.h [new file with mode: 0644]
src/libappimage/desktop_integration/integrator/Integrator.cpp [new file with mode: 0644]
src/libappimage/desktop_integration/integrator/Integrator.h [new file with mode: 0644]
src/libappimage/libappimage.c [new file with mode: 0644]
src/libappimage/libappimage.cpp [new file with mode: 0644]
src/libappimage/libappimage_legacy.cpp [new file with mode: 0644]
src/libappimage/type1.c [new file with mode: 0644]
src/libappimage/type2.c [new file with mode: 0644]
src/libappimage/utils/CMakeLists.txt [new file with mode: 0644]
src/libappimage/utils/DLHandle.h [new file with mode: 0644]
src/libappimage/utils/ElfFile.cpp [new file with mode: 0644]
src/libappimage/utils/ElfFile.h [new file with mode: 0644]
src/libappimage/utils/IconHandle.cpp [new file with mode: 0644]
src/libappimage/utils/IconHandle.h [new file with mode: 0644]
src/libappimage/utils/IconHandleCairoRsvg.cpp [new file with mode: 0644]
src/libappimage/utils/IconHandleCairoRsvg.h [new file with mode: 0644]
src/libappimage/utils/IconHandleDLOpenCairoRsvg.cpp [new file with mode: 0644]
src/libappimage/utils/IconHandleDLOpenCairoRsvg.h [new file with mode: 0644]
src/libappimage/utils/IconHandlePriv.h [new file with mode: 0644]
src/libappimage/utils/Logger.cpp [new file with mode: 0644]
src/libappimage/utils/Logger.h [new file with mode: 0644]
src/libappimage/utils/MagicBytesChecker.cpp [new file with mode: 0644]
src/libappimage/utils/MagicBytesChecker.h [new file with mode: 0644]
src/libappimage/utils/StringSanitizer.cpp [new file with mode: 0644]
src/libappimage/utils/StringSanitizer.h [new file with mode: 0644]
src/libappimage/utils/UrlEncoder.cpp [new file with mode: 0644]
src/libappimage/utils/UrlEncoder.h [new file with mode: 0644]
src/libappimage/utils/hashlib.cpp [new file with mode: 0644]
src/libappimage/utils/hashlib.h [new file with mode: 0644]
src/libappimage/utils/light_byteswap.h [new file with mode: 0644]
src/libappimage/utils/light_elf.h [new file with mode: 0644]
src/libappimage/utils/path_utils.cpp [new file with mode: 0644]
src/libappimage/utils/path_utils.h [new file with mode: 0644]
src/libappimage/utils/resources_extractor/PayloadEntriesCache.cpp [new file with mode: 0644]
src/libappimage/utils/resources_extractor/PayloadEntriesCache.h [new file with mode: 0644]
src/libappimage/utils/resources_extractor/ResourcesExtractor.cpp [new file with mode: 0644]
src/libappimage_hashlib/CMakeLists.txt [new file with mode: 0644]
src/libappimage_hashlib/include/hashlib.h [new file with mode: 0644]
src/libappimage_hashlib/include/md5.h [new file with mode: 0644]
src/libappimage_hashlib/md5.c [new file with mode: 0644]
src/libappimage_shared/CMakeLists.txt [new file with mode: 0644]
src/libappimage_shared/digest.c [new file with mode: 0644]
src/libappimage_shared/elf.c [new file with mode: 0644]
src/libappimage_shared/hexlify.c [new file with mode: 0644]
src/libappimage_shared/light_byteswap.h [new file with mode: 0644]
src/libappimage_shared/light_elf.h [new file with mode: 0644]
src/patches/patch-squashfuse.sh.in [new file with mode: 0755]
src/patches/squashfuse.patch [new file with mode: 0644]
src/patches/squashfuse_dlopen.c [new file with mode: 0644]
src/patches/squashfuse_dlopen.h [new file with mode: 0644]
src/patches/squashfuse_dlopen.patch [new file with mode: 0644]
src/xdg-basedir/CMakeLists.txt [new file with mode: 0644]
src/xdg-basedir/xdg-basedir.c [new file with mode: 0644]
src/xdg-basedir/xdg-basedir.h [new file with mode: 0644]
tests/CMakeLists.txt [new file with mode: 0644]
tests/client_app/CMakeLists.txt [new file with mode: 0644]
tests/client_app/main.c [new file with mode: 0644]
tests/data/AppImageExtract_6-x86_64.AppImage [new file with mode: 0755]
tests/data/AppImageExtract_6_no_magic_bytes-x86_64.AppImage [new file with mode: 0755]
tests/data/Cura.desktop [new file with mode: 0644]
tests/data/Echo-no-integrate-x86_64.AppImage [new file with mode: 0755]
tests/data/Echo-test1234-x86_64.AppImage [new file with mode: 0755]
tests/data/Echo-x86_64.AppImage [new file with mode: 0755]
tests/data/appimaged-i686.AppImage [new file with mode: 0755]
tests/data/appimagetool-x86_64.AppImage [new file with mode: 0755]
tests/data/broken-desktop-file-x86_64.AppImage [new file with mode: 0755]
tests/data/elffile [new file with mode: 0755]
tests/data/minimal.iso [new file with mode: 0644]
tests/data/squashfs-root/.DirIcon [new symlink]
tests/data/squashfs-root/AppRun [new file with mode: 0755]
tests/data/squashfs-root/echo.desktop [new file with mode: 0644]
tests/data/squashfs-root/usr/bin/echo [new file with mode: 0755]
tests/data/squashfs-root/utilities-terminal.png [new file with mode: 0644]
tests/data/squashfs-root/utilities-terminal.svg [new file with mode: 0644]
tests/libappimage/CMakeLists.txt [new file with mode: 0644]
tests/libappimage/TestLibappimage++.cpp [new file with mode: 0644]
tests/libappimage/core/impl/TestTraversalType1.cpp [new file with mode: 0644]
tests/libappimage/core/impl/TestTraversalType2.cpp [new file with mode: 0644]
tests/libappimage/desktop_integration/CMakeLists.txt [new file with mode: 0644]
tests/libappimage/desktop_integration/TestIntegrationManager.cpp [new file with mode: 0644]
tests/libappimage/desktop_integration/TestThumbnailer.cpp [new file with mode: 0644]
tests/libappimage/desktop_integration/integrator/TestDesktopEntryEditor.cpp [new file with mode: 0644]
tests/libappimage/desktop_integration/integrator/TestDesktopIntegration.cpp [new file with mode: 0644]
tests/libappimage/legacy/CMakeLists.txt [new file with mode: 0644]
tests/libappimage/legacy/fixtures.h [new file with mode: 0644]
tests/libappimage/legacy/test-xdg-basedir.cpp [new file with mode: 0644]
tests/libappimage/legacy/test_getsection.cpp [new file with mode: 0644]
tests/libappimage/legacy/test_libappimage.cpp [new file with mode: 0644]
tests/libappimage/legacy/test_shared.cpp [new file with mode: 0644]
tests/libappimage/utils/StringSanitizerTest.cpp [new file with mode: 0644]
tests/libappimage/utils/TestIconHandle.cpp [new file with mode: 0644]
tests/libappimage/utils/TestLogger.cpp [new file with mode: 0644]
tests/libappimage/utils/TestMagicBytesChecker.cpp [new file with mode: 0644]
tests/libappimage/utils/TestPayloadEntriesCache.cpp [new file with mode: 0644]
tests/libappimage/utils/TestResourcesExtractor.cpp [new file with mode: 0644]
tests/libappimage/utils/TestUtilsElf.cpp [new file with mode: 0644]

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644 (file)
index 0000000..ec56669
--- /dev/null
@@ -0,0 +1,49 @@
+name: CI
+
+on: [push, pull_request]
+
+jobs:
+  build-and-test:
+    strategy:
+      fail-fast: false
+      matrix:
+        DIST: [xenial, bionic, appimagebuild]
+        ARCH: [x86_64, i386]
+
+        include:
+          - DIST: bionic
+            ARCH: x86_64
+            BUILD_TYPE: coverage
+          - DIST: bionic
+            ARCH: x86_64
+            BUILD_TYPE: ""
+            LIBAPPIMAGE_SHARED_ONLY: 1
+
+    name: ${{ matrix.BUILD_TYPE }} ${{ matrix.DIST }} ${{ matrix.ARCH }} shared-only=${{ matrix.LIBAPPIMAGE_SHARED_ONLY }}
+    runs-on: ubuntu-latest
+    env:
+      ARCH: ${{ matrix.ARCH }}
+      DIST: ${{ matrix.DIST }}
+      BUILD_TYPE: ${{ matrix.DIST }}
+      LIBAPPIMAGE_SHARED_ONLY: ${{ matrix.LIBAPPIMAGE_SHARED_ONLY }}
+    steps:
+      - uses: actions/checkout@v2
+        with:
+          submodules: recursive
+      - name: Build libappimage and run tests
+        run: bash -ex ci/build-in-docker.sh
+        env:
+          DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
+          DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
+
+  docs:
+    name: docs
+    runs-on: ubuntu-latest
+    steps:
+      - name: Install dependencies
+        run: sudo apt-get install -y doxygen
+      - uses: actions/checkout@v2
+        with:
+          submodules: recursive
+      - name: Build API docs
+        run: cd docs/ && ./make.sh html
diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..be7f5b4
--- /dev/null
@@ -0,0 +1,8 @@
+*build*/
+cmake-build-*/
+.idea/
+Testing/
+docs/_build
+docs/api
+docs/doxyoutput
+*.swp
diff --git a/.gitmodules b/.gitmodules
new file mode 100644 (file)
index 0000000..cd6332f
--- /dev/null
@@ -0,0 +1,3 @@
+[submodule "lib/gtest"]
+       path = lib/gtest
+       url = https://github.com/google/googletest.git
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644 (file)
index 0000000..b9bb00a
--- /dev/null
@@ -0,0 +1,54 @@
+cmake_minimum_required(VERSION 3.4)
+
+project(libappimage)
+
+set(V_MAJOR 1)
+set(V_MINOR 0)
+set(V_PATCH 3)
+set(V_SUFFIX "")
+
+set(libappimage_VERSION ${V_MAJOR}.${V_MINOR}.${V_PATCH}${V_SUFFIX})
+
+# more versioning
+set(libappimage_SOVERSION ${V_MAJOR}.${V_MINOR})
+
+set(CMAKE_C_STANDARD 99)
+set(CMAKE_C_STANDARD_REQUIRED ON)
+
+set(CMAKE_CXX_STANDARD 11)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+# Optimize for size in release builds
+set(CMAKE_CXX_FLAGS_RELEASE "-Os -DNDEBUG -Wl,--gc-sections")
+
+set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${PROJECT_SOURCE_DIR}/cmake)
+
+include(cmake/reproducible_builds.cmake)
+
+option(LIBAPPIMAGE_DESKTOP_INTEGRATION_ENABLED "Enable desktop integration functions" On)
+option(LIBAPPIMAGE_THUMBNAILER_ENABLED "Enable thumbnailer functions" On)
+option(LIBAPPIMAGE_STANDALONE "Statically embbed dependencies" Off)
+option(LIBAPPIMAGE_SHARED_ONLY "Build and distribute shared and hashlib only (for use with AppImageKit and AppImageUpdate)" Off)
+option(ENABLE_COVERAGE "Enable tests code coverate target" Off)
+
+# some dependencies are only checked when testing is enabled
+include(CTest)
+
+include(cmake/tools.cmake)
+include(cmake/dependencies.cmake)
+
+if(ENABLE_COVERAGE)
+    message("Configuring project for code coverage mesurement")
+    include(CodeCoverage)
+    append_coverage_compiler_flags()
+endif()
+
+# used by e.g., Debian packaging infrastructure
+include(GNUInstallDirs)
+
+add_subdirectory(lib)
+add_subdirectory(src)
+
+if(BUILD_TESTING)
+    add_subdirectory(tests)
+endif()
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..f3777cc
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,28 @@
+MIT License
+
+If not stated otherwise within the individual file or subdirectory, the
+original source code in this repository is licensed as below. This does not
+necessarily apply for all dependencies. For the sake of clarity, this license
+does NOT apply to the contents of AppImages that anyone may create.
+Software contained inside an AppImage may be licensed under any license at the
+discretion of the respecive rights holder(s).
+
+Copyright (c) 2004-19 Simon Peter and the AppImage team
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..bf7750f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,42 @@
+# libappimage ![CI](https://github.com/AppImage/libappimage/workflows/CI/badge.svg) [![irc](https://img.shields.io/badge/IRC-%23AppImage%20on%20libera.chat-blue.svg)](https://web.libera.chat/#AppImage)
+
+This library is part of the [AppImage](https://github.com/AppImage/appimagekit/) project. It implements functionality for dealing with AppImage files. It is written in C++ and is using Boost.
+
+## Availablility
+
+libappimage is available in the following distributions:
+https://repology.org/project/libappimage/versions
+
+## Usage
+
+As a user, you normally do not need to deal with this library. Tools that use it (like [the optional `appimaged` daemon](https://github.com/AppImage/appimaged)) usually come with a bundled copy of it.
+
+## API documentation
+
+As a developer interested in using libappimage in your projects, please find the API documentation here:
+https://docs.appimage.org/api/libappimage/. Please note that if you are using libappimage in your project, we recommend bundling your private copy or linking statically to it, since the versions provided by distributions may be outdated.
+
+## Building
+
+If for some reason you need to do a local development build, on a deb-based system (tested on Ubuntu xenial) do:
+
+```
+sudo apt-get -y install automake cmake libtool libcairo-dev libfuse-dev git
+git clone https://github.com/AppImage/libappimage --recursive
+cd ./libappimage/
+mkdir build
+cd build
+cmake .. -DBUILD_TESTING:bool=False
+make
+sudo make install
+cd ..
+```
+
+## Contributing
+
+Your contributions are welcome.
+
+If you make or suggest changes to this code, please test that the resulting executables (like [the `appimaged` daemon](https://github.com/AppImage/appimaged)) are still working properly.
+
+
+If you have questions, AppImage developers are on #AppImage on irc.libera.chat.
diff --git a/ci/Dockerfile.bionic-i386 b/ci/Dockerfile.bionic-i386
new file mode 100644 (file)
index 0000000..1d0fc92
--- /dev/null
@@ -0,0 +1,11 @@
+FROM i386/ubuntu:bionic
+
+ENV ARCH=i386 DIST=bionic CI=1
+
+COPY ./install-deps.sh /
+RUN bash -xe install-deps.sh
+
+# create unprivileged user for non-build-script use of this image
+# build-in-docker.sh will likely not use this one, as it enforces the caller's uid inside the container
+RUN adduser --system --group build
+USER build
diff --git a/ci/Dockerfile.bionic-x86_64 b/ci/Dockerfile.bionic-x86_64
new file mode 100644 (file)
index 0000000..fd21ac3
--- /dev/null
@@ -0,0 +1,11 @@
+FROM ubuntu:bionic
+
+ENV ARCH=x86_64 DIST=bionic CI=1
+
+COPY ./install-deps.sh /
+RUN bash -xe install-deps.sh
+
+# create unprivileged user for non-build-script use of this image
+# build-in-docker.sh will likely not use this one, as it enforces the caller's uid inside the container
+RUN adduser --system --group build
+USER build
diff --git a/ci/Dockerfile.xenial-i386 b/ci/Dockerfile.xenial-i386
new file mode 100644 (file)
index 0000000..6a8c8b4
--- /dev/null
@@ -0,0 +1,11 @@
+FROM i386/ubuntu:xenial
+
+ENV ARCH=i386 DIST=xenial CI=1
+
+COPY ./install-deps.sh /
+RUN bash -xe install-deps.sh
+
+# create unprivileged user for non-build-script use of this image
+# build-in-docker.sh will likely not use this one, as it enforces the caller's uid inside the container
+RUN adduser --system --group build
+USER build
diff --git a/ci/Dockerfile.xenial-x86_64 b/ci/Dockerfile.xenial-x86_64
new file mode 100644 (file)
index 0000000..8d39fa1
--- /dev/null
@@ -0,0 +1,11 @@
+FROM ubuntu:xenial
+
+ENV ARCH=x86_64 DIST=xenial CI=1
+
+COPY ./install-deps.sh /
+RUN bash -xe install-deps.sh
+
+# create unprivileged user for non-build-script use of this image
+# build-in-docker.sh will likely not use this one, as it enforces the caller's uid inside the container
+RUN adduser --system --group build
+USER build
diff --git a/ci/build-and-test.sh b/ci/build-and-test.sh
new file mode 100755 (executable)
index 0000000..7e35e89
--- /dev/null
@@ -0,0 +1,59 @@
+#! /bin/bash
+
+set -e
+set -x
+
+# use RAM disk if possible
+if [ "$CI" == "" ] && [ -d /dev/shm ]; then
+    TEMP_BASE=/dev/shm
+else
+    TEMP_BASE=/tmp
+fi
+
+BUILD_DIR="$(mktemp -d -p "$TEMP_BASE" libappimage-build-XXXXXX)"
+
+cleanup () {
+    if [ -d "$BUILD_DIR" ]; then
+        rm -rf "$BUILD_DIR"
+    fi
+}
+
+trap cleanup EXIT
+
+# store repo root as variable
+REPO_ROOT="$(readlink -f "$(dirname "$(dirname "$0")")")"
+OLD_CWD="$(readlink -f .)"
+
+pushd "$BUILD_DIR"
+
+EXTRA_CMAKE_ARGS=()
+if [[ "$LIBAPPIMAGE_SHARED_ONLY" != "" ]]; then
+    # shared only builds do not provide any tests
+    EXTRA_CMAKE_ARGS+=("-DLIBAPPIMAGE_SHARED_ONLY=ON" "-DBUILD_TESTING=OFF")
+fi
+
+# configure build
+if [ "$BUILD_TYPE" == "coverage" ]; then
+    cmake "$REPO_ROOT" -DCMAKE_INSTALL_LIBDIR=lib -DENABLE_COVERAGE=On "${EXTRA_CMAKE_ARGS[@]}"
+    make -j"$(nproc)" coverage
+else
+    cmake "$REPO_ROOT" -DCMAKE_INSTALL_LIBDIR=lib "${EXTRA_CMAKE_ARGS[@]}"
+
+    # build binaries
+    make -j"$(nproc)"
+
+    # run all unit tests
+    ctest -V
+fi
+
+# install libappimage
+DESTDIR="$BUILD_DIR"/libappimage make install
+
+if [[ "$LIBAPPIMAGE_SHARED_ONLY" == "" ]]; then
+    # do integration test
+    mkdir "$BUILD_DIR"/client_app_build
+    pushd "$BUILD_DIR"/client_app_build
+    cmake -DCMAKE_PREFIX_PATH="$BUILD_DIR"/libappimage/usr/local/lib/cmake/libappimage "$REPO_ROOT"/tests/client_app/ "${EXTRA_CMAKE_ARGS[@]}"
+    make
+    ./client_app
+fi
diff --git a/ci/build-docker-image.sh b/ci/build-docker-image.sh
new file mode 100755 (executable)
index 0000000..7b74e01
--- /dev/null
@@ -0,0 +1,40 @@
+#! /bin/bash
+
+if [[ "$DIST" == "" ]] || [[ "$ARCH" == "" ]]; then
+    echo "Usage: env ARCH=... DIST=... bash $0"
+    exit 1
+fi
+
+set -x
+set -e
+
+# the other script sources this script, therefore we have to support that use case
+if [[ "${BASH_SOURCE[*]}" != "" ]]; then
+    this_dir="$(readlink -f "$(dirname "${BASH_SOURCE[0]}")")"
+else
+    this_dir="$(readlink -f "$(dirname "$0")")"
+fi
+
+# needed to keep user ID in and outside Docker in sync to be able to write to workspace directory
+image=quay.io/appimage/libappimage-build:"$DIST"-"$ARCH"
+dockerfile="$this_dir"/Dockerfile."$DIST"-"$ARCH"
+
+if [ ! -f "$dockerfile" ]; then
+    echo "Error: $dockerfile could not be found"
+    exit 1
+fi
+
+# speed up build by pulling last built image from quay.io and building the docker file using the old image as a base
+docker pull "$image" || true
+# if the image hasn't changed, this should be a no-op
+docker build --pull --cache-from "$image" -t "$image" -f "$dockerfile" "$this_dir"
+
+# push built image as cache for future builds to registry
+# we can do that immediately once the image has been built successfully; if its definition ever changes it will be
+# rebuilt anyway
+# credentials shall only be available on (protected) master branch
+set +x
+if [[ "$DOCKER_USERNAME" != "" ]]; then
+    echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin quay.io
+    docker push "$image"
+fi
diff --git a/ci/build-in-docker.sh b/ci/build-in-docker.sh
new file mode 100755 (executable)
index 0000000..5edd72d
--- /dev/null
@@ -0,0 +1,45 @@
+#! /bin/bash
+
+if [[ "$DIST" == "" ]] || [[ "$ARCH" == "" ]]; then
+    echo "Usage: env ARCH=... DIST=... bash $0"
+    exit 1
+fi
+
+set -x
+set -e
+
+cd "$(readlink -f "$(dirname "$0")")"
+
+if [[ "$DIST" != appimagebuild* ]]; then
+    # sets variables $image, $dockerfile
+    source build-docker-image.sh
+else
+    image=quay.io/appimage/appimagebuild:centos7-"$ARCH"
+    docker pull "$image"
+fi
+
+DOCKER_OPTS=()
+# fix for https://stackoverflow.com/questions/51195528/rcc-error-in-resource-qrc-cannot-find-file-png
+if [ "$CI" != "" ]; then
+    DOCKER_OPTS+=("--security-opt" "seccomp:unconfined")
+fi
+
+# only if there's more than 3G of free space in RAM, we can build in a RAM disk
+if [[ "$GITHUB_ACTIONS" != "" ]]; then
+    echo "Building on GitHub actions, which does not support --tmpfs flag -> building on regular disk"
+elif [[ "$(free -m  | grep "Mem:" | awk '{print $4}')" -gt 3072 ]]; then
+    echo "Host system has enough free memory -> building in RAM disk"
+    DOCKER_OPTS+=("--tmpfs /docker-ramdisk:exec,mode=777")
+else
+    echo "Host system does not have enough free memory -> building on regular disk"
+fi
+
+# run the build with the current user to
+#   a) make sure root is not required for builds
+#   b) allow the build scripts to "mv" the binaries into the /out directory
+uid="$(id -u)"
+# run build
+docker run -e LIBAPPIMAGE_SHARED_ONLY -e DIST -e ARCH -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ID -e BUILD_TYPE --rm -i --user "$uid" \
+     "${DOCKER_OPTS[@]}" -v "$(readlink -f ..):/ws" \
+     "$image" \
+     bash -xc "export CI=1 && cd /ws && source ci/build-and-test.sh"
diff --git a/ci/install-deps.sh b/ci/install-deps.sh
new file mode 100755 (executable)
index 0000000..2f2e45e
--- /dev/null
@@ -0,0 +1,70 @@
+#! /bin/bash
+
+set -e
+
+if [[ "$ARCH" == "" ]]; then
+    echo "Usage: env ARCH=... bash $0"
+    exit 2
+fi
+
+if [[ "$CI" == "" ]]; then
+    echo "Caution: this script is supposed to run inside a (disposable) CI environment"
+    echo "It will alter a system, and should not be run on workstations or alike"
+    echo "You can export CI=1 to prevent this error from being shown again"
+    exit 3
+fi
+
+case "$ARCH" in
+    x86_64|i386|armhf|arm64)
+        ;;
+    *)
+        echo "Error: unsupported architecture: $ARCH"
+        exit 4
+        ;;
+esac
+
+case "$DIST" in
+    xenial|bionic)
+        ;;
+    *)
+        echo "Error: unsupported distribution: $DIST"
+        exit 5
+        ;;
+esac
+
+set -x
+
+packages=(
+    libfuse-dev
+    desktop-file-utils
+    ca-certificates
+    gcc-multilib
+    g++-multilib
+    make
+    build-essential
+    git
+    automake
+    autoconf
+    libtool
+    patch
+    wget
+    vim-common
+    desktop-file-utils
+    pkg-config
+    libarchive-dev
+    librsvg2-dev
+    librsvg2-bin
+    liblzma-dev
+    cmake
+    lcov
+    gcovr
+)
+
+apt-get update
+apt-get -y --no-install-recommends install "${packages[@]}"
+
+# install more recent CMake version
+wget https://artifacts.assassinate-you.net/prebuilt-cmake/continuous/cmake-v3.19.1-ubuntu_"$DIST"-"$ARCH".tar.gz -qO- | \
+    tar xz -C/usr/local --strip-components=1
+
+
diff --git a/cmake/CodeCoverage.cmake b/cmake/CodeCoverage.cmake
new file mode 100644 (file)
index 0000000..fbb5eb0
--- /dev/null
@@ -0,0 +1,310 @@
+# Copyright (c) 2012 - 2017, Lars Bilke
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its contributors
+#    may be used to endorse or promote products derived from this software without
+#    specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+# CHANGES:
+#
+# 2012-01-31, Lars Bilke
+# - Enable Code Coverage
+#
+# 2013-09-17, Joakim Sรถderberg
+# - Added support for Clang.
+# - Some additional usage instructions.
+#
+# 2016-02-03, Lars Bilke
+# - Refactored functions to use named parameters
+#
+# 2017-06-02, Lars Bilke
+# - Merged with modified version from github.com/ufz/ogs
+#
+#
+# USAGE:
+#
+# 1. Copy this file into your cmake modules path.
+#
+# 2. Add the following line to your CMakeLists.txt:
+#      include(CodeCoverage)
+#
+# 3. Append necessary compiler flags:
+#      APPEND_COVERAGE_COMPILER_FLAGS()
+#
+# 4. If you need to exclude additional directories from the report, specify them
+#    using the COVERAGE_LCOV_EXCLUDES variable before calling SETUP_TARGET_FOR_COVERAGE_LCOV.
+#    Example:
+#      set(COVERAGE_LCOV_EXCLUDES 'dir1/*' 'dir2/*')
+#
+# 5. Use the functions described below to create a custom make target which
+#    runs your test executable and produces a code coverage report.
+#
+# 6. Build a Debug build:
+#      cmake -DCMAKE_BUILD_TYPE=Debug ..
+#      make
+#      make my_coverage_target
+#
+
+include(CMakeParseArguments)
+
+# Check prereqs
+find_program(GCOV_PATH gcov)
+find_program(LCOV_PATH NAMES lcov lcov.bat lcov.exe lcov.perl)
+find_program(GENHTML_PATH NAMES genhtml genhtml.perl genhtml.bat)
+find_program(GCOVR_PATH gcovr PATHS ${CMAKE_SOURCE_DIR}/scripts/test)
+find_program(SIMPLE_PYTHON_EXECUTABLE python)
+
+if(NOT GCOV_PATH)
+    message(FATAL_ERROR "gcov not found! Aborting...")
+endif() # NOT GCOV_PATH
+
+if("${CMAKE_CXX_COMPILER_ID}" MATCHES "(Apple)?[Cc]lang")
+    if("${CMAKE_CXX_COMPILER_VERSION}" VERSION_LESS 3)
+        message(FATAL_ERROR "Clang version must be 3.0.0 or greater! Aborting...")
+    endif()
+elseif(NOT CMAKE_COMPILER_IS_GNUCXX)
+    message(FATAL_ERROR "Compiler is not GNU gcc! Aborting...")
+endif()
+
+set(COVERAGE_COMPILER_FLAGS "-g -O0 --coverage -fprofile-arcs -ftest-coverage"
+    CACHE INTERNAL "")
+
+set(CMAKE_CXX_FLAGS_COVERAGE
+    ${COVERAGE_COMPILER_FLAGS}
+    CACHE STRING "Flags used by the C++ compiler during coverage builds."
+    FORCE)
+set(CMAKE_C_FLAGS_COVERAGE
+    ${COVERAGE_COMPILER_FLAGS}
+    CACHE STRING "Flags used by the C compiler during coverage builds."
+    FORCE)
+set(CMAKE_EXE_LINKER_FLAGS_COVERAGE
+    ""
+    CACHE STRING "Flags used for linking binaries during coverage builds."
+    FORCE)
+set(CMAKE_SHARED_LINKER_FLAGS_COVERAGE
+    ""
+    CACHE STRING "Flags used by the shared libraries linker during coverage builds."
+    FORCE)
+mark_as_advanced(
+    CMAKE_CXX_FLAGS_COVERAGE
+    CMAKE_C_FLAGS_COVERAGE
+    CMAKE_EXE_LINKER_FLAGS_COVERAGE
+    CMAKE_SHARED_LINKER_FLAGS_COVERAGE)
+
+if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
+    message(WARNING "Code coverage results with an optimised (non-Debug) build may be misleading")
+endif() # NOT CMAKE_BUILD_TYPE STREQUAL "Debug"
+
+if(CMAKE_C_COMPILER_ID STREQUAL "GNU")
+    link_libraries(gcov)
+else()
+    set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage")
+endif()
+
+# Defines a target for running and collection code coverage information
+# Builds dependencies, runs the given executable and outputs reports.
+# NOTE! The executable should always have a ZERO as exit code otherwise
+# the coverage generation will not complete.
+#
+# SETUP_TARGET_FOR_COVERAGE_LCOV(
+#     NAME testrunner_coverage                    # New target name
+#     EXECUTABLE testrunner -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR
+#     DEPENDENCIES testrunner                     # Dependencies to build first
+# )
+function(SETUP_TARGET_FOR_COVERAGE_LCOV)
+
+    set(options NONE)
+    set(oneValueArgs NAME)
+    set(multiValueArgs EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES LCOV_ARGS GENHTML_ARGS)
+    cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
+
+    if(NOT LCOV_PATH)
+        message(FATAL_ERROR "lcov not found! Aborting...")
+    endif() # NOT LCOV_PATH
+
+    if(NOT GENHTML_PATH)
+        message(FATAL_ERROR "genhtml not found! Aborting...")
+    endif() # NOT GENHTML_PATH
+
+    # Setup target
+    add_custom_target(
+        ${Coverage_NAME}
+
+        # Cleanup lcov
+        COMMAND ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} -directory . --zerocounters
+        # Create baseline to make sure untouched files show up in the report
+        COMMAND ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} -c -i -d . -o ${Coverage_NAME}.base
+
+        # Run tests
+        COMMAND ${Coverage_EXECUTABLE} ${Coverage_EXECUTABLE_ARGS}
+
+        # Capturing lcov counters and generating report
+        COMMAND ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} --directory . --capture --output-file ${Coverage_NAME}.info
+        # add baseline counters
+        COMMAND ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} -a ${Coverage_NAME}.base -a ${Coverage_NAME}.info --output-file ${Coverage_NAME}.total
+        COMMAND ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} --remove ${Coverage_NAME}.total ${COVERAGE_LCOV_EXCLUDES} --output-file ${PROJECT_BINARY_DIR}/${Coverage_NAME}.info.cleaned
+        COMMAND ${GENHTML_PATH} ${Coverage_GENHTML_ARGS} -o ${Coverage_NAME} ${PROJECT_BINARY_DIR}/${Coverage_NAME}.info.cleaned
+        COMMAND ${CMAKE_COMMAND} -E remove ${Coverage_NAME}.base ${Coverage_NAME}.total ${PROJECT_BINARY_DIR}/${Coverage_NAME}.info.cleaned
+
+        WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
+        DEPENDS ${Coverage_DEPENDENCIES}
+        COMMENT "Resetting code coverage counters to zero.\nProcessing code coverage counters and generating report."
+    )
+
+    # Show where to find the lcov info report
+    add_custom_command(
+        TARGET ${Coverage_NAME} POST_BUILD
+        COMMAND ;
+        COMMENT "Lcov code coverage info report saved in ${Coverage_NAME}.info."
+    )
+
+    # Show info where to find the report
+    add_custom_command(
+        TARGET ${Coverage_NAME} POST_BUILD
+        COMMAND ;
+        COMMENT "Open ./${Coverage_NAME}/index.html in your browser to view the coverage report."
+    )
+
+endfunction() # SETUP_TARGET_FOR_COVERAGE_LCOV
+
+# Defines a target for running and collection code coverage information
+# Builds dependencies, runs the given executable and outputs reports.
+# NOTE! The executable should always have a ZERO as exit code otherwise
+# the coverage generation will not complete.
+#
+# SETUP_TARGET_FOR_COVERAGE_GCOVR_XML(
+#     NAME ctest_coverage                    # New target name
+#     EXECUTABLE ctest -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR
+#     DEPENDENCIES executable_target         # Dependencies to build first
+# )
+function(SETUP_TARGET_FOR_COVERAGE_GCOVR_XML)
+
+    set(options NONE)
+    set(oneValueArgs NAME)
+    set(multiValueArgs EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES)
+    cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
+
+    if(NOT SIMPLE_PYTHON_EXECUTABLE)
+        message(FATAL_ERROR "python not found! Aborting...")
+    endif() # NOT SIMPLE_PYTHON_EXECUTABLE
+
+    if(NOT GCOVR_PATH)
+        message(FATAL_ERROR "gcovr not found! Aborting...")
+    endif() # NOT GCOVR_PATH
+
+    # Combine excludes to several -e arguments
+    set(GCOVR_EXCLUDES "")
+    foreach(EXCLUDE ${COVERAGE_GCOVR_EXCLUDES})
+        list(APPEND GCOVR_EXCLUDES "-e")
+        list(APPEND GCOVR_EXCLUDES "${EXCLUDE}")
+    endforeach()
+
+    add_custom_target(
+        ${Coverage_NAME}
+        # Run tests
+        ${Coverage_EXECUTABLE} ${Coverage_EXECUTABLE_ARGS}
+
+        # Running gcovr
+        COMMAND ${GCOVR_PATH} --xml
+        -r ${PROJECT_SOURCE_DIR} ${GCOVR_EXCLUDES}
+        --object-directory=${PROJECT_BINARY_DIR}
+        -o ${Coverage_NAME}.xml
+        WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
+        DEPENDS ${Coverage_DEPENDENCIES}
+        COMMENT "Running gcovr to produce Cobertura code coverage report."
+    )
+
+    # Show info where to find the report
+    add_custom_command(
+        TARGET ${Coverage_NAME} POST_BUILD
+        COMMAND ;
+        COMMENT "Cobertura code coverage report saved in ${Coverage_NAME}.xml."
+    )
+
+endfunction() # SETUP_TARGET_FOR_COVERAGE_GCOVR_XML
+
+# Defines a target for running and collection code coverage information
+# Builds dependencies, runs the given executable and outputs reports.
+# NOTE! The executable should always have a ZERO as exit code otherwise
+# the coverage generation will not complete.
+#
+# SETUP_TARGET_FOR_COVERAGE_GCOVR_HTML(
+#     NAME ctest_coverage                    # New target name
+#     EXECUTABLE ctest -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR
+#     DEPENDENCIES executable_target         # Dependencies to build first
+# )
+function(SETUP_TARGET_FOR_COVERAGE_GCOVR_HTML)
+
+    set(options NONE)
+    set(oneValueArgs NAME)
+    set(multiValueArgs EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES)
+    cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
+
+    if(NOT SIMPLE_PYTHON_EXECUTABLE)
+        message(FATAL_ERROR "python not found! Aborting...")
+    endif() # NOT SIMPLE_PYTHON_EXECUTABLE
+
+    if(NOT GCOVR_PATH)
+        message(FATAL_ERROR "gcovr not found! Aborting...")
+    endif() # NOT GCOVR_PATH
+
+    # Combine excludes to several -e arguments
+    set(GCOVR_EXCLUDES "")
+    foreach(EXCLUDE ${COVERAGE_GCOVR_EXCLUDES})
+        list(APPEND GCOVR_EXCLUDES "-e")
+        list(APPEND GCOVR_EXCLUDES "${EXCLUDE}")
+    endforeach()
+
+    add_custom_target(
+        ${Coverage_NAME}
+        # Run tests
+        ${Coverage_EXECUTABLE} ${Coverage_EXECUTABLE_ARGS}
+
+        # Create folder
+        COMMAND ${CMAKE_COMMAND} -E make_directory ${PROJECT_BINARY_DIR}/${Coverage_NAME}
+
+        # Running gcovr
+        COMMAND ${GCOVR_PATH} --html --html-details
+        -r ${PROJECT_SOURCE_DIR} ${GCOVR_EXCLUDES}
+        --object-directory=${PROJECT_BINARY_DIR}
+        -o ${Coverage_NAME}/index.html
+        WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
+        DEPENDS ${Coverage_DEPENDENCIES}
+        COMMENT "Running gcovr to produce HTML code coverage report."
+    )
+
+    # Show info where to find the report
+    add_custom_command(
+        TARGET ${Coverage_NAME} POST_BUILD
+        COMMAND ;
+        COMMENT "Open ./${Coverage_NAME}/index.html in your browser to view the coverage report."
+    )
+
+endfunction() # SETUP_TARGET_FOR_COVERAGE_GCOVR_HTML
+
+function(APPEND_COVERAGE_COMPILER_FLAGS)
+    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE)
+    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE)
+    message(STATUS "Appending code coverage compiler flags: ${COVERAGE_COMPILER_FLAGS}")
+endfunction() # APPEND_COVERAGE_COMPILER_FLAGS
diff --git a/cmake/Modules/Findsquashfuse.cmake b/cmake/Modules/Findsquashfuse.cmake
new file mode 100644 (file)
index 0000000..24529a4
--- /dev/null
@@ -0,0 +1,18 @@
+cmake_minimum_required(VERSION 3.0)
+
+find_path(squashfuse_H_DIR
+    NAMES squashfuse.h
+    HINTS ${CMAKE_INSTALL_PREFIX}
+    PATH_SUFFIXES include include/linux
+)
+
+if(NOT squashfuse_H_DIR)
+    message(FATAL_ERROR "squashfuse.h not found")
+endif()
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(squashfuse DEFAULT_MSG SQUASHFUSE_INCLUDE_DIR SQUASHFUSE_LIBRARY_DIR)
+
+add_library(squashfuse IMPORTED SHARED)
+set_property(TARGET squashfuse PROPERTY IMPORTED_LOCATION ${squashfuse_LIBRARY})
+set_property(TARGET squashfuse PROPERTY INTERFACE_INCLUDE_DIRECTORIES "${squashfuse_H_DIR}")
diff --git a/cmake/dependencies.cmake b/cmake/dependencies.cmake
new file mode 100644 (file)
index 0000000..a1509c9
--- /dev/null
@@ -0,0 +1,233 @@
+# >= 3.2 required for ExternalProject_Add_StepDependencies
+cmake_minimum_required(VERSION 3.2)
+
+include(${CMAKE_CURRENT_LIST_DIR}/scripts.cmake)
+
+# we use the template both when building from source and in the exported configs
+# to make the template work, we have to copy the scripts file to the (future) CMAKE_CURRENT_LIST_DIR (i.e., the parent
+# directory of configure_file's target)
+file(
+    COPY "${CMAKE_CURRENT_LIST_DIR}/scripts.cmake"
+    DESTINATION "${PROJECT_BINARY_DIR}/cmake/"
+)
+configure_file(
+    "${CMAKE_CURRENT_LIST_DIR}/imported_dependencies.cmake.in"
+    "${PROJECT_BINARY_DIR}/cmake/imported_dependencies.cmake"
+    @ONLY
+)
+include("${PROJECT_BINARY_DIR}/cmake/imported_dependencies.cmake")
+
+if(USE_CCACHE)
+    message(STATUS "Using CCache to build AppImageKit dependencies")
+    # TODO: find way to use find_program with all possible paths
+    # (might differ from distro to distro)
+    # these work on Debian and Ubuntu:
+    set(CC "/usr/lib/ccache/gcc")
+    set(CXX "/usr/lib/ccache/g++")
+else()
+    set(CC "${CMAKE_C_COMPILER}")
+    set(CXX "${CMAKE_CXX_COMPILER}")
+endif()
+
+set(CFLAGS ${DEPENDENCIES_CFLAGS})
+set(CPPFLAGS ${DEPENDENCIES_CPPFLAGS})
+set(LDFLAGS ${DEPENDENCIES_LDFLAGS})
+
+
+set(USE_SYSTEM_XZ OFF CACHE BOOL "Use system xz/liblzma instead of building our own")
+
+if (NOT LIBAPPIMAGE_SHARED_ONLY)
+    if(NOT USE_SYSTEM_XZ)
+        message(STATUS "Downloading and building xz")
+
+        ExternalProject_Add(
+            xz-EXTERNAL
+            URL https://netcologne.dl.sourceforge.net/project/lzmautils/xz-5.2.5.tar.gz
+            URL_HASH SHA512=7443674247deda2935220fbc4dfc7665e5bb5a260be8ad858c8bd7d7b9f0f868f04ea45e62eb17c0a5e6a2de7c7500ad2d201e2d668c48ca29bd9eea5a73a3ce
+            CONFIGURE_COMMAND CC=${CC} CXX=${CXX} CFLAGS=${CFLAGS} CPPFLAGS=${CPPFLAGS} LDFLAGS=${LDFLAGS} <SOURCE_DIR>/configure --with-pic --disable-shared --enable-static --prefix=<INSTALL_DIR> --libdir=<INSTALL_DIR>/lib ${EXTRA_CONFIGURE_FLAGS} --disable-xz --disable-xzdec
+            BUILD_COMMAND ${MAKE}
+            INSTALL_COMMAND ${MAKE} install
+            UPDATE_DISCONNECTED On
+        )
+
+        import_external_project(
+            TARGET_NAME xz
+            EXT_PROJECT_NAME xz-EXTERNAL
+            LIBRARY_DIRS <INSTALL_DIR>/lib/
+            LIBRARIES "<INSTALL_DIR>/lib/liblzma.a"
+            INCLUDE_DIRS "<SOURCE_DIR>/src/liblzma/api/"
+        )
+    else()
+        message(STATUS "Using system xz")
+
+        import_pkgconfig_target(TARGET_NAME xz PKGCONFIG_TARGET liblzma)
+    endif()
+
+
+    # as distros don't provide suitable squashfuse and squashfs-tools, those dependencies are bundled in, can, and should
+    # be used from this repository for AppImageKit
+    # for distro packaging, it can be linked to an existing package just fine
+    set(USE_SYSTEM_SQUASHFUSE OFF CACHE BOOL "Use system libsquashfuse instead of building our own")
+
+    if(NOT USE_SYSTEM_SQUASHFUSE)
+        message(STATUS "Downloading and building squashfuse")
+
+        # Check if fuse is installed to provide early error reports
+        import_pkgconfig_target(TARGET_NAME libfuse PKGCONFIG_TARGET fuse)
+
+        # TODO: implement out-of-source builds for squashfuse, as for the other dependencies
+        configure_file(
+            ${CMAKE_CURRENT_SOURCE_DIR}/src/patches/patch-squashfuse.sh.in
+            ${CMAKE_CURRENT_BINARY_DIR}/patch-squashfuse.sh
+            @ONLY
+        )
+
+        ExternalProject_Add(
+            squashfuse-EXTERNAL
+            GIT_REPOSITORY https://github.com/vasi/squashfuse/
+            GIT_TAG 1f98030
+            UPDATE_COMMAND ""  # make sure CMake won't try to fetch updates unnecessarily and hence rebuild the dependency every time
+            PATCH_COMMAND bash -xe ${CMAKE_CURRENT_BINARY_DIR}/patch-squashfuse.sh
+            CONFIGURE_COMMAND ${LIBTOOLIZE} --force
+            COMMAND env ACLOCAL_FLAGS="-I /usr/share/aclocal" aclocal
+            COMMAND ${AUTOHEADER}
+            COMMAND ${AUTOMAKE} --force-missing --add-missing
+            COMMAND ${AUTORECONF} -fi || true
+            COMMAND ${SED} -i "/PKG_CHECK_MODULES.*/,/,:./d" configure  # https://github.com/vasi/squashfuse/issues/12
+            COMMAND ${SED} -i "s/typedef off_t sqfs_off_t/typedef int64_t sqfs_off_t/g" common.h  # off_t's size might differ, see https://stackoverflow.com/a/9073762
+            COMMAND CC=${CC} CXX=${CXX} CFLAGS=${CFLAGS} LDFLAGS=${LDFLAGS} <SOURCE_DIR>/configure --disable-demo --disable-high-level --without-lzo --without-lz4 --prefix=<INSTALL_DIR> --libdir=<INSTALL_DIR>/lib --with-xz=${xz_PREFIX} ${EXTRA_CONFIGURE_FLAGS}
+            COMMAND ${SED} -i "s|XZ_LIBS = -llzma |XZ_LIBS = -Bstatic ${xz_LIBRARIES}/|g" Makefile
+            BUILD_COMMAND ${MAKE}
+            BUILD_IN_SOURCE ON
+            INSTALL_COMMAND ${MAKE} install
+            UPDATE_DISCONNECTED On
+        )
+
+        import_external_project(
+            TARGET_NAME libsquashfuse
+            EXT_PROJECT_NAME squashfuse-EXTERNAL
+            LIBRARIES "<SOURCE_DIR>/.libs/libsquashfuse.a;<SOURCE_DIR>/.libs/libsquashfuse_ll.a;<SOURCE_DIR>/.libs/libfuseprivate.a"
+            INCLUDE_DIRS "<SOURCE_DIR>"
+        )
+    else()
+        message(STATUS "Using system squashfuse")
+
+        import_pkgconfig_target(TARGET_NAME libsquashfuse PKGCONFIG_TARGET squashfuse)
+    endif()
+
+
+    set(USE_SYSTEM_LIBARCHIVE OFF CACHE BOOL "Use system libarchive instead of building our own")
+
+    if(NOT USE_SYSTEM_LIBARCHIVE)
+        message(STATUS "Downloading and building libarchive")
+
+        ExternalProject_Add(
+            libarchive-EXTERNAL
+            URL https://www.libarchive.org/downloads/libarchive-3.3.1.tar.gz
+            URL_HASH SHA512=90702b393b6f0943f42438e277b257af45eee4fa82420431f6a4f5f48bb846f2a72c8ff084dc3ee9c87bdf8b57f4d8dddf7814870fe2604fe86c55d8d744c164
+            CONFIGURE_COMMAND CC=${CC} CXX=${CXX} CFLAGS=${CFLAGS} CPPFLAGS=${CPPFLAGS} LDFLAGS=${LDFLAGS} <SOURCE_DIR>/configure --with-pic --disable-shared --enable-static --disable-bsdtar --disable-bsdcat --disable-bsdcpio --with-zlib --without-bz2lib --without-iconv --without-lz4 --without-lzma --without-lzo2 --without-nettle --without-openssl --without-xml2 --without-expat --prefix=<INSTALL_DIR> --libdir=<INSTALL_DIR>/lib ${EXTRA_CONFIGURE_FLAGS}
+            BUILD_COMMAND ${MAKE}
+            INSTALL_COMMAND ${MAKE} install
+            UPDATE_DISCONNECTED On
+        )
+
+        import_external_project(
+            TARGET_NAME libarchive
+            EXT_PROJECT_NAME libarchive-EXTERNAL
+            LIBRARIES "<INSTALL_DIR>/lib/libarchive.a"
+            INCLUDE_DIRS "<INSTALL_DIR>/include/"
+        )
+    else()
+        message(STATUS "Using system libarchive")
+
+        import_find_pkg_target(libarchive LibArchive LibArchive)
+    endif()
+
+
+    #### build dependency configuration ####
+
+    # only have to build custom xz when not using system libxz
+    if(TARGET xz-EXTERNAL)
+        if(TARGET squashfuse-EXTERNAL)
+            ExternalProject_Add_StepDependencies(squashfuse-EXTERNAL configure xz-EXTERNAL)
+        endif()
+    endif()
+
+    ## Boost
+    if(NOT USE_SYSTEM_BOOST)
+        message(STATUS "Downloading and building boost")
+
+        if("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "i386")
+            set(BOOST_B2_TARGET_CONFIG architecture=x86 address-model=32)
+        endif()
+
+        # support for clang compiler
+        # if the toolset is not explicitly specified, ./bootstrap.sh will not generate the ./b2 script
+        string(TOLOWER ${CMAKE_CXX_COMPILER_ID} boost_compiler_id)
+
+        # of course, there has to be some exception to that snippet
+        # CMake's "gnu" toolset is called "gcc" in boost
+        if(${boost_compiler_id} STREQUAL gnu)
+            set(boost_compiler_id gcc)
+        endif()
+
+        ExternalProject_Add(
+            boost-EXTERNAL
+            URL https://downloads.sourceforge.net/project/boost/boost/1.69.0/boost_1_69_0.tar.gz
+            URL_HASH SHA256=9a2c2819310839ea373f42d69e733c339b4e9a19deab6bfec448281554aa4dbb
+            CONFIGURE_COMMAND ./bootstrap.sh --with-libraries=filesystem,system,thread --with-toolset=${boost_compiler_id}
+            BUILD_COMMAND ./b2 ${BOOST_B2_TARGET_CONFIG} cxxflags=-fPIC ${CPPFLAGS} cflags=-fPIC ${CFLAGS} link=static
+            INSTALL_COMMAND ""
+            BUILD_IN_SOURCE 1
+            UPDATE_DISCONNECTED On
+        )
+
+        import_external_project(
+            TARGET_NAME Boost::filesystem
+            EXT_PROJECT_NAME boost-EXTERNAL
+            LIBRARIES "<BINARY_DIR>/stage/lib/libboost_filesystem.a;<BINARY_DIR>/stage/lib/libboost_system.a"
+            INCLUDE_DIRS "<BINARY_DIR>"
+        )
+
+    else()
+        find_package(Boost REQUIRED COMPONENTS filesystem)
+    endif()
+
+
+    ## XdgUtils
+
+    if(USE_SYSTEM_XDGUTILS)
+        find_package(XdgUtils REQUIRED COMPONENTS DesktopEntry BaseDir)
+    else()
+        message(STATUS "Downloading and building XdgUtils")
+
+        ExternalProject_Add(
+            XdgUtils-EXTERNAL
+            GIT_REPOSITORY https://github.com/azubieta/xdg-utils-cxx.git
+            GIT_TAG master
+            GIT_SHALLOW On
+            CMAKE_ARGS
+            -DCMAKE_POSITION_INDEPENDENT_CODE=On
+            -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
+            -DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS}
+            -DCMAKE_C_FLAGS=${CMAKE_C_FLAGS}
+
+            INSTALL_COMMAND ""
+            UPDATE_DISCONNECTED On
+        )
+
+        import_external_project(
+            TARGET_NAME XdgUtils::DesktopEntry
+            EXT_PROJECT_NAME XdgUtils-EXTERNAL
+            LIBRARIES "<BINARY_DIR>/src/DesktopEntry/libXdgUtilsDesktopEntry.a;"
+            INCLUDE_DIRS "<SOURCE_DIR>/include"
+        )
+
+        import_external_project(
+            TARGET_NAME XdgUtils::BaseDir
+            EXT_PROJECT_NAME XdgUtils-EXTERNAL
+            LIBRARIES "<BINARY_DIR>/src/BaseDir/libXdgUtilsBaseDir.a;"
+            INCLUDE_DIRS "<SOURCE_DIR>/include"
+        )
+    endif()
+endif()
diff --git a/cmake/imported_dependencies.cmake.in b/cmake/imported_dependencies.cmake.in
new file mode 100644 (file)
index 0000000..402fa4c
--- /dev/null
@@ -0,0 +1,13 @@
+include(${CMAKE_CURRENT_LIST_DIR}/scripts.cmake)
+
+# the names of the targets need to differ from the library filenames
+# this is especially an issue with libcairo, where the library is called libcairo
+# therefore, all libs imported this way have been prefixed with lib
+if(NOT @LIBAPPIMAGE_SHARED_ONLY@)
+    import_pkgconfig_target(TARGET_NAME libglib PKGCONFIG_TARGET glib-2.0>=2.40)
+    import_pkgconfig_target(TARGET_NAME libgobject PKGCONFIG_TARGET gobject-2.0>=2.40)
+    import_pkgconfig_target(TARGET_NAME libgio PKGCONFIG_TARGET gio-2.0>=2.40)
+    import_pkgconfig_target(TARGET_NAME libzlib PKGCONFIG_TARGET zlib)
+    import_pkgconfig_target(TARGET_NAME libcairo PKGCONFIG_TARGET cairo)
+    import_pkgconfig_target(TARGET_NAME librsvg PKGCONFIG_TARGET librsvg-2.0)
+endif()
diff --git a/cmake/libappimage.pc.in b/cmake/libappimage.pc.in
new file mode 100644 (file)
index 0000000..32682d8
--- /dev/null
@@ -0,0 +1,12 @@
+prefix=@CMAKE_INSTALL_PREFIX@
+# Use prefix as base path to make the package relocatable
+libdir=${prefix}/@CMAKE_INSTALL_LIBDIR@
+includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@
+
+Name: @PROJECT_NAME@
+Description: AppImage management and desktop integration
+Version: @libappimage_VERSION@
+URL: https://github.com/AppImage/libappimage
+
+Libs: -L${libdir} -lappimage
+Cflags: -I${includedir}
diff --git a/cmake/libappimageConfig.cmake.in b/cmake/libappimageConfig.cmake.in
new file mode 100644 (file)
index 0000000..6d4c7ab
--- /dev/null
@@ -0,0 +1,24 @@
+# - Config file for the AppImage package
+# Exported Targets
+#  - libappimage
+#  - libappimage_shared
+#
+# Exported Variables (DEPRECATED use the exported targets instead)
+#  LIBAPPIMAGE_INCLUDE_DIRS - include directories for LIBAPPIMAGE
+#  LIBAPPIMAGE_LIBRARIES    - libraries to link against
+
+@PACKAGE_INIT@
+
+# Compute paths
+get_filename_component(LIBAPPIMAGE_CMAKE_DIR ${CMAKE_CURRENT_LIST_FILE} PATH)
+
+# Import dependencies implicitly required by libappimageTargets.cmake
+include(${LIBAPPIMAGE_CMAKE_DIR}/imported_dependencies.cmake)
+
+# Our library dependencies (contains definitions for IMPORTED targets)
+include(${LIBAPPIMAGE_CMAKE_DIR}/libappimageTargets.cmake)
+
+if(NOT @LIBAPPIMAGE_SHARED_ONLY@)
+    get_target_property(LIBAPPIMAGE_INCLUDE_DIRS libappimage INTERFACE_INCLUDE_DIRECTORIES)
+    set(LIBAPPIMAGE_LIBRARIES libappimage)
+endif()
diff --git a/cmake/libappimageConfigVersion.cmake.in b/cmake/libappimageConfigVersion.cmake.in
new file mode 100644 (file)
index 0000000..2e3cd8a
--- /dev/null
@@ -0,0 +1,11 @@
+set(PACKAGE_VERSION "@libappimage_VERSION@")
+# Check whether the requested PACKAGE_FIND_VERSION is compatible
+if("${PACKAGE_VERSION}" VERSION_LESS "${PACKAGE_FIND_VERSION}")
+  set(PACKAGE_VERSION_COMPATIBLE FALSE)
+else()
+  set(PACKAGE_VERSION_COMPATIBLE TRUE)
+  if ("${PACKAGE_VERSION}" VERSION_EQUAL "${PACKAGE_FIND_VERSION}")
+    set(PACKAGE_VERSION_EXACT TRUE)
+  endif()
+endif()
diff --git a/cmake/reproducible_builds.cmake b/cmake/reproducible_builds.cmake
new file mode 100644 (file)
index 0000000..7fefaf4
--- /dev/null
@@ -0,0 +1,34 @@
+# this little snippet makes sure that no absolute paths end up in the binaries built by CMake
+# it will replace such paths with relative ones
+# see https://reproducible-builds.org/docs/build-path/ for more information
+
+cmake_minimum_required(VERSION 3.4)
+
+include(CheckCCompilerFlag)
+
+if(CMAKE_BUILD_TYPE STREQUAL Release)
+    message(STATUS "Release build detected, enabling reproducible builds mode")
+    get_filename_component(abs_source_path ${PROJECT_SOURCE_DIR} ABSOLUTE)
+    file(RELATIVE_PATH rel_source_path ${PROJECT_BINARY_DIR} ${PROJECT_SOURCE_DIR})
+
+    set(map_fix ${abs_source_path}=${rel_source_path})
+
+    # can only add flags when the compiler supports them
+    # known working compilers: GCC >= 8
+    foreach(type debug macro)
+        set(flag_name -f${type}-prefix-map)
+        set(flags ${flag_name}=${map_fix})
+
+        check_c_compiler_flag(${flags} ${type}_prefix_map_flag_available)
+
+        if(${type}_prefix_map_flag_available)
+            set(extra_flags "${extra_flags} ${flags}")
+        else()
+            message(WARNING "${flag_name} not available, cannot enable full reproducible builds mode")
+        endif()
+    endforeach()
+
+    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${extra_flags}")
+    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${extra_flags}")
+    set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${extra_flags}")
+endif()
diff --git a/cmake/scripts.cmake b/cmake/scripts.cmake
new file mode 100644 (file)
index 0000000..f792d1b
--- /dev/null
@@ -0,0 +1,262 @@
+cmake_minimum_required(VERSION 3.2)
+
+include(ExternalProject)
+
+# searches for absolute paths of libraries, applying LIBRARY_DIRS
+# CMake prefers absolute paths of libraries in non-standard locations, apparently
+# see FindPkgConfig.cmake's _pkg_find_libs for more information
+#
+# positional parameters:
+#  - libraries: name of variable containing list of libraries
+#  - library_dirs
+function(apply_library_dirs libraries library_dirs)
+    foreach(library ${${libraries}})
+        find_library(${library}_path ${library} PATHS ${${library_dirs}} NO_DEFAULT_PATH)
+        if(NOT ${library}_path)
+            list(APPEND new_libraries ${library})
+        else()
+            list(APPEND new_libraries ${${library}_path})
+        endif()
+    endforeach()
+    set(${libraries} ${new_libraries} PARENT_SCOPE)
+    unset(new_libraries)
+endfunction()
+
+# imports a library from the standard set of variables CMake creates when using its pkg-config module or find_package
+# this is code shared by import_pkgconfig_target and import_external_project, hence it's been extracted into a separate
+# CMake function
+#
+# partly inspired by https://github.com/Kitware/CMake/blob/master/Modules/FindPkgConfig.cmake#L187
+#
+# positional parameters:
+#  - target_name: name of the target that should be created
+#  - variable_prefix: prefix of the variable that should be used to create the target from
+function(import_library_from_prefix target_name variable_prefix)
+    if(TARGET ${target_name})
+        message(WARNING "Target exists already, skipping")
+        return()
+    endif()
+
+    add_library(${target_name} INTERFACE IMPORTED GLOBAL)
+
+    if(${variable_prefix}_INCLUDE_DIRS)
+        # need to create directories before setting INTERFACE_INCLUDE_DIRECTORIES, otherwise CMake will complain
+        # possibly related: https://cmake.org/Bug/view.php?id=15052
+        foreach(dir ${${variable_prefix}_INCLUDE_DIRS})
+            if(NOT EXISTS ${dir})
+                if(${dir} MATCHES ${CMAKE_BINARY_DIR})
+                    file(MAKE_DIRECTORY ${dir})
+                    list(APPEND include_dirs ${dir})
+                endif()
+            else()
+                list(APPEND include_dirs ${dir})
+            endif()
+        endforeach()
+        set_property(TARGET ${target_name} PROPERTY INTERFACE_INCLUDE_DIRECTORIES ${include_dirs})
+        unset(include_dirs)
+    endif()
+
+    # if library dirs are set, figure out absolute paths to libraries, like CMake's FindPkgConfig module does
+    if(${variable_prefix}_LIBRARY_DIRS)
+        apply_library_dirs(${variable_prefix}_LIBRARIES ${variable_prefix}_LIBRARY_DIRS)
+    endif()
+
+    set_property(TARGET ${target_name} PROPERTY INTERFACE_LINK_LIBRARIES ${${variable_prefix}_LIBRARIES})
+
+    if(${variable_prefix}_CFLAGS_OTHER)
+        set_property(TARGET ${target_name} PROPERTY INTERFACE_COMPILE_OPTIONS ${${variable_prefix}_CFLAGS_OTHER})
+    endif()
+
+    # export some of the imported properties with the target name as prefix
+    # this is necessary to allow the other external projects, which are not built with CMake or not within the same
+    # CMake context, to link to the libraries built as external projects (or the system ones, depending on the build
+    # configuration)
+    set(${target_name}_INCLUDE_DIRS ${${variable_prefix}_INCLUDE_DIRS} CACHE INTERNAL "")
+    set(${target_name}_LIBRARIES ${${variable_prefix}_LIBRARIES} CACHE INTERNAL "")
+    set(${target_name}_LIBRARY_DIRS ${${variable_prefix}_LIBRARY_DIRS} CACHE INTERNAL "")
+    # TODO: the following might not always apply
+    set(${target_name}_PREFIX ${CMAKE_INSTALL_PREFIX}/lib CACHE INTERNAL "")
+endfunction()
+
+
+# imports a library using pkg-config
+#
+# positional parameters:
+#  - target_name: name of the target that we shall create for you
+#  - pkg_config_target: librar(y name to pass to pkg-config (may include a version)
+function(import_pkgconfig_target)
+    set(keywords STATIC OPTIONAL)
+    set(oneValueArgs TARGET_NAME PKGCONFIG_TARGET)
+    cmake_parse_arguments(IMPORT_PKGCONFIG_TARGET "${keywords}" "${oneValueArgs}" "" "${ARGN}")
+
+    # check whether parameters have been set
+    if(NOT IMPORT_PKGCONFIG_TARGET_TARGET_NAME)
+        message(FATAL_ERROR "TARGET_NAME parameter missing, but is required")
+    endif()
+    if(NOT IMPORT_PKGCONFIG_TARGET_PKGCONFIG_TARGET)
+        message(FATAL_ERROR "PKGCONFIG_TARGET parameter missing, but is required")
+    endif()
+
+    find_package(PkgConfig REQUIRED)
+
+    set(type "shared")
+    if(IMPORT_PKGCONFIG_TARGET_STATIC)
+        set(type "static")
+    endif()
+
+    message(STATUS "Importing target ${IMPORT_PKGCONFIG_TARGET_TARGET_NAME} via pkg-config (${IMPORT_PKGCONFIG_TARGET_PKGCONFIG_TARGET}, ${type})")
+
+    if(NOT IMPORT_PKGCONFIG_TARGET_OPTIONAL)
+        set(extra_args REQUIRED)
+    endif()
+
+    pkg_check_modules(${IMPORT_PKGCONFIG_TARGET_TARGET_NAME}-IMPORTED ${IMPORT_PKGCONFIG_TARGET_PKGCONFIG_TARGET} ${extra_args})
+
+    if(NOT ${IMPORT_PKGCONFIG_TARGET_TARGET_NAME}-IMPORTED_FOUND AND IMPORT_PKGCONFIG_TARGET_OPTIONAL)
+        return()
+    endif()
+
+    if(IMPORT_PKGCONFIG_TARGET_STATIC)
+        set(prefix ${IMPORT_PKGCONFIG_TARGET_TARGET_NAME}-IMPORTED_STATIC)
+    else()
+        set(prefix ${IMPORT_PKGCONFIG_TARGET_TARGET_NAME}-IMPORTED)
+    endif()
+
+    import_library_from_prefix(${IMPORT_PKGCONFIG_TARGET_TARGET_NAME} ${prefix})
+endfunction()
+
+function(import_find_pkg_target target_name pkg_name variable_prefix)
+    message(STATUS "Importing target ${target_name} via find_package (${pkg_name})")
+
+    find_package(${pkg_name})
+    if(NOT ${variable_prefix}_FOUND)
+        message(FATAL_ERROR "${pkg_name} could not be found on the system. You will have to either install it, or use the bundled package.")
+    endif()
+
+    import_library_from_prefix(${target_name} ${variable_prefix})
+endfunction()
+
+
+# imports a library from an existing external project
+#
+# required parameters:
+#  - TARGET_NAME:
+function(import_external_project)
+    set(oneValueArgs TARGET_NAME EXT_PROJECT_NAME)
+    set(multiValueArgs LIBRARIES INCLUDE_DIRS LIBRARY_DIRS)
+    cmake_parse_arguments(IMPORT_EXTERNAL_PROJECT "" "${oneValueArgs}" "${multiValueArgs}" "${ARGN}")
+
+    # check whether parameters have been set
+    if(NOT IMPORT_EXTERNAL_PROJECT_TARGET_NAME)
+        message(FATAL_ERROR "TARGET_NAME parameter missing, but is required")
+    endif()
+    if(NOT IMPORT_EXTERNAL_PROJECT_EXT_PROJECT_NAME)
+        message(FATAL_ERROR "EXT_PROJECT_NAME parameter missing, but is required")
+    endif()
+    if(NOT IMPORT_EXTERNAL_PROJECT_LIBRARIES)
+        message(FATAL_ERROR "LIBRARIES parameter missing, but is required")
+    endif()
+    if(NOT IMPORT_EXTERNAL_PROJECT_INCLUDE_DIRS)
+        message(FATAL_ERROR "INCLUDE_DIRS parameter missing, but is required")
+    endif()
+
+    if(TARGET ${target_name})
+        message(WARNING "Target exists already, skipping")
+        return()
+    endif()
+
+    add_library(${IMPORT_EXTERNAL_PROJECT_TARGET_NAME} INTERFACE IMPORTED GLOBAL)
+
+    ExternalProject_Get_Property(${IMPORT_EXTERNAL_PROJECT_EXT_PROJECT_NAME} SOURCE_DIR)
+    ExternalProject_Get_Property(${IMPORT_EXTERNAL_PROJECT_EXT_PROJECT_NAME} INSTALL_DIR)
+    ExternalProject_Get_Property(${IMPORT_EXTERNAL_PROJECT_EXT_PROJECT_NAME} BINARY_DIR)
+
+    # "evaluate" patterns in the passed arguments by using some string replacing magic
+    # this makes it easier to use this function, as some external project properties don't need to be evaluated and
+    # passed beforehand, and should reduce the amount of duplicate code in this file
+    foreach(item ITEMS
+        IMPORT_EXTERNAL_PROJECT_EXT_PROJECT_NAME
+        IMPORT_EXTERNAL_PROJECT_LIBRARIES
+        IMPORT_EXTERNAL_PROJECT_INCLUDE_DIRS
+        IMPORT_EXTERNAL_PROJECT_LIBRARY_DIRS)
+
+        # create new variable with fixed string...
+        string(REPLACE "<SOURCE_DIR>" "${SOURCE_DIR}" "${item}-out" "${${item}}")
+        # ... and set the original value to the new value
+        set(${item} "${${item}-out}")
+
+        # create new variable with fixed string...
+        string(REPLACE "<INSTALL_DIR>" "${INSTALL_DIR}" "${item}-out" "${${item}}")
+        # ... and set the original value to the new value
+        set(${item} "${${item}-out}")
+
+        # create new variable with fixed string...
+        string(REPLACE "<BINARY_DIR>" "${BINARY_DIR}" "${item}-out" "${${item}}")
+        # ... and set the original value to the new value
+        set(${item} "${${item}-out}")
+    endforeach()
+
+    # if library dirs are set, figure out absolute paths to libraries, like CMake's FindPkgConfig module does
+    if(${IMPORT_EXTERNAL_PROJECT_LIBRARY_DIRS})
+        apply_library_dirs(IMPORT_EXTERNAL_PROJECT_LIBRARIES IMPORT_EXTERNAL_PROJECT_LIBRARY_DIRS)
+    endif()
+
+    set_property(TARGET ${IMPORT_EXTERNAL_PROJECT_TARGET_NAME} PROPERTY INTERFACE_LINK_LIBRARIES "${IMPORT_EXTERNAL_PROJECT_LIBRARIES}")
+
+    if(IMPORT_EXTERNAL_PROJECT_INCLUDE_DIRS)
+        # need to create directories before setting INTERFACE_INCLUDE_DIRECTORIES, otherwise CMake will complain
+        # possibly related: https://cmake.org/Bug/view.php?id=15052
+
+        foreach(dir ${IMPORT_EXTERNAL_PROJECT_INCLUDE_DIRS})
+            if(NOT EXISTS ${dir})
+                if(${dir} MATCHES ${CMAKE_BINARY_DIR})
+                    file(MAKE_DIRECTORY ${dir})
+                    list(APPEND include_dirs ${dir})
+                endif()
+            else()
+                list(APPEND include_dirs ${dir})
+            endif()
+        endforeach()
+        set_property(TARGET ${IMPORT_EXTERNAL_PROJECT_TARGET_NAME} PROPERTY INTERFACE_INCLUDE_DIRECTORIES ${include_dirs})
+        unset(include_dirs)
+    endif()
+
+    # finally, add a depenceny on the external project to make sure it's built
+    add_dependencies(${IMPORT_EXTERNAL_PROJECT_TARGET_NAME} "${IMPORT_EXTERNAL_PROJECT_EXT_PROJECT_NAME}")
+
+    # read external project properties, and export them with the target name as prefix
+    # this is necessary to allow the other external projects, which are not built with CMake or not within the same
+    # CMake context, to link to the libraries built as external projects (or the system ones, depending on the build
+    # configuration)
+    set(${IMPORT_EXTERNAL_PROJECT_TARGET_NAME}_INCLUDE_DIRS "${IMPORT_EXTERNAL_PROJECT_INCLUDE_DIRS}" CACHE INTERNAL "")
+    set(${IMPORT_EXTERNAL_PROJECT_TARGET_NAME}_LIBRARIES "${IMPORT_EXTERNAL_PROJECT_LIBRARIES}" CACHE INTERNAL "")
+    set(${IMPORT_EXTERNAL_PROJECT_TARGET_NAME}_LIBRARY_DIRS "${IMPORT_EXTERNAL_PROJECT_LIBRARY_DIRS}" CACHE INTERNAL "")
+    set(${IMPORT_EXTERNAL_PROJECT_TARGET_NAME}_PREFIX ${INSTALL_DIR} CACHE INTERNAL "")
+endfunction()
+
+# @brief Configure a libappimage module by setting
+#
+# Sets set to the given <target> the public headers, the compile definitions and the include directories. Which are
+# common to all modules.
+function(configure_libappimage_module target)
+    # targets are called lib* already, so CMake shouldn't add another lib prefix to the actual files
+    set_target_properties(${target}
+        PROPERTIES PREFIX ""
+        POSITION_INDEPENDENT_CODE ON
+    )
+
+    target_compile_definitions(${target}
+        # Support Large Files
+        PRIVATE -D_FILE_OFFSET_BITS=64
+        PRIVATE -D_LARGEFILE_SOURCE
+
+        PRIVATE -DGIT_COMMIT="${GIT_COMMIT}"
+        PRIVATE -DENABLE_BINRELOC
+    )
+
+    target_include_directories(${target}
+        PUBLIC $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include/>
+        PRIVATE $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/src/libappimage>
+        INTERFACE $<INSTALL_INTERFACE:include/>
+    )
+endfunction()
diff --git a/cmake/tools.cmake b/cmake/tools.cmake
new file mode 100644 (file)
index 0000000..2b2cade
--- /dev/null
@@ -0,0 +1,58 @@
+if(TOOLS_PREFIX)
+    message(STATUS "TOOLS_PREFIX set, looking for programs with prefix ${TOOLS_PREFIX}")
+endif()
+
+# first of all, make sure required programs are available
+function(check_program)
+    set(keywords FORCE_PREFIX)
+    set(oneValueArgs NAME)
+    cmake_parse_arguments(ARG "${keywords}" "${oneValueArgs}" "" "${ARGN}")
+
+    if(NOT ARG_NAME)
+        message(FATAL_ERROR "NAME argument required for check_program")
+    endif()
+
+    if(TOOLS_PREFIX)
+        set(prefix ${TOOLS_PREFIX})
+    endif()
+
+    message(STATUS "Checking for program ${ARG_NAME}")
+
+    string(TOUPPER ${ARG_NAME} name_upper)
+
+    if(prefix)
+        # try prefixed version first
+        find_program(${name_upper} ${prefix}${ARG_NAME})
+    endif()
+
+    # try non-prefixed version
+    if(NOT ${name_upper})
+        if(TOOLS_PREFIX AND ARG_FORCE_PREFIX)
+            message(FATAL_ERROR "TOOLS_PREFIX set, but could not find program with prefix in PATH (FORCE_PREFIX is set)")
+        endif()
+
+        find_program(${name_upper} ${ARG_NAME})
+
+        if(NOT ${name_upper})
+            message(FATAL_ERROR "Could not find required program ${ARG_NAME}.")
+        endif()
+    endif()
+
+    message(STATUS "Found program ${ARG_NAME}: ${${name_upper}}")
+
+    mark_as_advanced(${name_upper})
+endfunction()
+
+if (NOT LIBAPPIMAGE_SHARED_ONLY)
+    check_program(NAME aclocal)
+    check_program(NAME autoheader)
+    check_program(NAME automake)
+    check_program(NAME autoreconf)
+    check_program(NAME libtoolize)
+    check_program(NAME patch)
+    check_program(NAME sed)
+    check_program(NAME make)
+endif()
+if(BUILD_TESTING)
+    check_program(NAME desktop-file-validate)
+endif()
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644 (file)
index 0000000..3e5da5c
--- /dev/null
@@ -0,0 +1,3 @@
+venv/
+.venv/
+_build/
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644 (file)
index 0000000..7ad4d79
--- /dev/null
@@ -0,0 +1,24 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = sphinx-build
+SOURCEDIR     = .
+BUILDDIR      = _build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+       @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile clean
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+       @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+
+clean:
+       rm -rf doxyoutput/ api/
+       @$(SPHINXBUILD) -M clean "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644 (file)
index 0000000..99f1887
--- /dev/null
@@ -0,0 +1,82 @@
+# Configuration file for the Sphinx documentation builder.
+#
+# This file only contains a selection of the most common options. For a full
+# list see the documentation:
+# http://www.sphinx-doc.org/en/master/config
+
+# -- Path setup --------------------------------------------------------------
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+# import os
+# import sys
+# sys.path.insert(0, os.path.abspath('.'))
+
+
+# -- Project information -----------------------------------------------------
+
+project = 'libappimage'
+copyright = '2019, The AppImage Project'
+author = 'The AppImage Project'
+
+# The full version, including alpha/beta/rc tags
+release = '1.0.0'
+
+# -- General configuration ---------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+    'breathe',
+    'exhale'
+]
+
+# Setup the breathe extension
+breathe_projects = {
+    "libappimage": "./doxyoutput/xml"
+}
+
+# Setup the exhale extension
+exhale_args = {
+    # These arguments are required
+    "containmentFolder": "./api",
+    "rootFileName": "library_root.rst",
+    "rootFileTitle": "libappimage API",
+    "doxygenStripFromPath": "..",
+    # Suggested optional arguments
+    "createTreeView": True,
+    "exhaleExecutesDoxygen": True,
+    "exhaleDoxygenStdin": "INPUT = ../include"
+}
+
+
+# Tell sphinx what the primary language being documented is.
+primary_domain = 'cpp'
+
+# Tell sphinx what the pygments highlight language should be.
+highlight_language = 'cpp'
+
+breathe_default_project = "libappimage"
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+#
+html_theme = "sphinx_rtd_theme"
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644 (file)
index 0000000..f86c158
--- /dev/null
@@ -0,0 +1,23 @@
+.. libappimage documentation master file, created by
+   sphinx-quickstart on Fri May 31 17:03:51 2019.
+   You can adapt this file completely to your liking, but it should at least
+   contain the root `toctree` directive.
+
+Welcome to libappimage's documentation!
+=======================================
+
+This library is part of the AppImage project. It implements functionality for dealing with AppImage files.
+It is written in C++ and is using Boost.
+
+.. toctree::
+   :maxdepth: 2
+   :caption: Contents:
+
+   api/library_root
+
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`search`
diff --git a/docs/make.bat b/docs/make.bat
new file mode 100644 (file)
index 0000000..27f573b
--- /dev/null
@@ -0,0 +1,35 @@
+@ECHO OFF\r
+\r
+pushd %~dp0\r
+\r
+REM Command file for Sphinx documentation\r
+\r
+if "%SPHINXBUILD%" == "" (\r
+       set SPHINXBUILD=sphinx-build\r
+)\r
+set SOURCEDIR=.\r
+set BUILDDIR=_build\r
+\r
+if "%1" == "" goto help\r
+\r
+%SPHINXBUILD% >NUL 2>NUL\r
+if errorlevel 9009 (\r
+       echo.\r
+       echo.The 'sphinx-build' command was not found. Make sure you have Sphinx\r
+       echo.installed, then set the SPHINXBUILD environment variable to point\r
+       echo.to the full path of the 'sphinx-build' executable. Alternatively you\r
+       echo.may add the Sphinx directory to PATH.\r
+       echo.\r
+       echo.If you don't have Sphinx installed, grab it from\r
+       echo.http://sphinx-doc.org/\r
+       exit /b 1\r
+)\r
+\r
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\r
+goto end\r
+\r
+:help\r
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\r
+\r
+:end\r
+popd\r
diff --git a/docs/make.sh b/docs/make.sh
new file mode 100755 (executable)
index 0000000..66e912d
--- /dev/null
@@ -0,0 +1,27 @@
+#! /bin/bash
+
+set -e
+
+CUR_DIR=$(readlink -f $(dirname "$0"))
+
+cd "$CUR_DIR"
+
+VENV="$CUR_DIR"/.venv
+
+if [ ! -d "$VENV" ] || [ ! -e "$VENV"/bin/activate ]; then
+    echo $(tput bold)$(tput setaf 2)"Creating new virtual environment in $VENV"$(tput sgr0)
+    PYTHON=python3
+    which python3.6 &>/dev/null && PYTHON=python3.6
+    "$PYTHON" -m venv "$VENV"
+fi
+
+source "$VENV"/bin/activate
+
+# this snippet should allow us to call pip install only if the requirements file has been touched
+if [ ! -f "$VENV"/requirements.txt ] || [ $(sha256sum requirements.txt | cut -d' ' -f1) != $(sha256sum "$VENV"/requirements.txt | cut -d' ' -f1) ]; then
+    echo $(tput bold)$(tput setaf 2)"Requirements updated, reinstalling"$(tput sgr0)
+    cp requirements.txt "$VENV"/requirements.txt
+    pip install -U -r "$VENV"/requirements.txt
+fi
+
+make "$@"
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644 (file)
index 0000000..143f3e3
--- /dev/null
@@ -0,0 +1,3 @@
+exhale
+sphinx>=2.0
+sphinx_rtd_theme
diff --git a/include/appimage/appimage++.h b/include/appimage/appimage++.h
new file mode 100644 (file)
index 0000000..b70912e
--- /dev/null
@@ -0,0 +1,12 @@
+#pragma once
+
+/**
+ * C++ headers aggregation to differentiate from the C only interface.
+ */
+
+#include <appimage/config.h>
+#include <appimage/core/AppImage.h>
+
+#ifdef LIBAPPIMAGE_DESKTOP_INTEGRATION_ENABLED
+#include <appimage/desktop_integration/IntegrationManager.h>
+#endif
diff --git a/include/appimage/appimage.h b/include/appimage/appimage.h
new file mode 100644 (file)
index 0000000..e619bf5
--- /dev/null
@@ -0,0 +1,104 @@
+#pragma once
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <unistd.h>
+#include <stdbool.h>
+
+// Configuration header
+#include <appimage/config.h>
+
+// include header of shared library, which contains more appimage_ functions
+#include <appimage/appimage_shared.h>
+
+// include legacy functions
+#include <appimage/appimage_legacy.h>
+
+/* Return the md5 hash constructed according to
+* https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html#THUMBSAVE
+* This can be used to identify files that are related to a given AppImage at a given location */
+char *appimage_get_md5(char const* path);
+
+/**
+ * Find the offset at which starts the payload of the AppImage pointed by <path>
+ * @param path
+ * @return
+ */
+off_t appimage_get_payload_offset(char const* path);
+
+/* Check if a file is an AppImage. Returns the image type if it is, or -1 if it isn't */
+int appimage_get_type(const char* path, bool verbose);
+
+/*
+ * Finds the desktop file of a registered AppImage and returns the path
+ * Returns NULL if the desktop file can't be found, which should only occur when the AppImage hasn't been registered yet
+ */
+char* appimage_registered_desktop_file_path(const char* path, char* md5, bool verbose);
+
+/* Extract a given file from the appimage following the symlinks until a concrete file is found */
+void appimage_extract_file_following_symlinks(const char* appimage_file_path, const char* file_path, const char* target_file_path);
+
+/* Read a given file from the AppImage into a freshly allocated buffer following symlinks
+ * Buffer must be free()d after usage
+ * */
+bool appimage_read_file_into_buffer_following_symlinks(const char* appimage_file_path, const char* file_path, char** buffer, unsigned long* buf_size);
+
+
+/* List files contained in the AppImage file.
+ * Returns: a newly allocated char** ended at NULL. If no files ware found also is returned a {NULL}
+ *
+ * You should ALWAYS take care of releasing this using `appimage_string_list_free`.
+ * */
+char** appimage_list_files(const char* path);
+
+/* Releases memory of a string list (a.k.a. list of pointers to char arrays allocated in heap memory). */
+void appimage_string_list_free(char** list);
+
+/*
+ * Checks whether an AppImage's desktop file has set Terminal=true.
+ *
+ * Returns >0 if set, 0 if not set, <0 on errors.
+ */
+int appimage_is_terminal_app(const char* path);
+
+#ifdef LIBAPPIMAGE_DESKTOP_INTEGRATION_ENABLED
+/*
+ * Checks whether an AppImage's desktop file has set X-AppImage-Version=false.
+ * Useful to check whether the author of an AppImage doesn't want it to be integrated.
+ *
+ * Returns >0 if set, 0 if not set, <0 on errors.
+ */
+int appimage_shall_not_be_integrated(const char* path);
+
+/*
+ * Check whether an AppImage has been registered in the system
+ */
+bool appimage_is_registered_in_system(const char* path);
+
+/*
+ * Register an AppImage in the system
+ * Returns 0 on success, non-0 otherwise.
+ */
+int appimage_register_in_system(const char *path, bool verbose);
+
+/* Unregister an AppImage in the system */
+int appimage_unregister_in_system(const char *path, bool verbose);
+
+
+#ifdef LIBAPPIMAGE_THUMBNAILER_ENABLED
+/*
+ * Create AppImage thumbnail according to
+ * https://specifications.freedesktop.org/thumbnail-spec/0.8.0/index.html
+ * Returns true on success, false otherwise.
+ */
+bool appimage_create_thumbnail(const char* appimage_file_path, bool verbose);
+#endif // LIBAPPIMAGE_THUMBNAILER_ENABLED
+
+#endif // LIBAPPIMAGE_DESKTOP_INTEGRATION_ENABLED
+
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/include/appimage/appimage_legacy.h b/include/appimage/appimage_legacy.h
new file mode 100644 (file)
index 0000000..e3895bc
--- /dev/null
@@ -0,0 +1,71 @@
+#pragma once
+
+// system
+#include <stdio.h>
+
+// local
+#include <appimage/config.h>
+
+/**
+ * All of the functions in this header are deprecated and must not be used in newly written code
+ */
+
+/*
+ * Calculate the size of an ELF file on disk based on the information in its header
+ *
+ * Example:
+ *
+ * ls -l   126584
+ *
+ * Calculation using the values also reported by readelf -h:
+ * Start of section headers    e_shoff         124728
+ * Size of section headers             e_shentsize     64
+ * Number of section headers   e_shnum         29
+ *
+ * e_shoff + ( e_shentsize * e_shnum ) =       126584
+ */
+ssize_t appimage_get_elf_size(const char* fname) __attribute__ ((deprecated));
+
+
+/*
+ * Checks whether a type 1 AppImage's desktop file has set Terminal=true.
+ *
+ * Returns >0 if set, 0 if not set, <0 on errors.
+ */
+int appimage_type1_is_terminal_app(const char* path) __attribute__ ((deprecated));
+
+/*
+ * Checks whether a type 2 AppImage's desktop file has set Terminal=true.
+ *
+ * Returns >0 if set, 0 if not set, <0 on errors.
+ */
+int appimage_type2_is_terminal_app(const char* path) __attribute__ ((deprecated));
+
+#ifdef LIBAPPIMAGE_DESKTOP_INTEGRATION_ENABLED
+/*
+ * Checks whether a type 1 AppImage's desktop file has set X-AppImage-Version=false.
+ * Useful to check whether the author of an AppImage doesn't want it to be integrated.
+ *
+ * Returns >0 if set, 0 if not set, <0 on errors.
+ */
+int appimage_type1_shall_not_be_integrated(const char* path) __attribute__ ((deprecated));
+
+/*
+ * Checks whether a type 2 AppImage's desktop file has set X-AppImage-Version=false.
+ * Useful to check whether the author of an AppImage doesn't want it to be integrated.
+ *
+ * Returns >0 if set, 0 if not set, <0 on errors.
+ */
+int appimage_type2_shall_not_be_integrated(const char* path) __attribute__ ((deprecated));
+
+/* Register a type 1 AppImage in the system
+ * DEPRECATED don't use in newly written code. Use appimage_is_registered_in_system instead.
+ * */
+bool appimage_type1_register_in_system(const char* path, bool verbose) __attribute__ ((deprecated));
+
+/* Register a type 2 AppImage in the system
+ * DEPRECATED don't use in newly written code. Use appimage_is_registered_in_system instead.
+ * */
+bool appimage_type2_register_in_system(const char* path, bool verbose) __attribute__ ((deprecated));
+
+#endif // LIBAPPIMAGE_DESKTOP_INTEGRATION_ENABLED
diff --git a/include/appimage/appimage_shared.h b/include/appimage/appimage_shared.h
new file mode 100644 (file)
index 0000000..2b7c18b
--- /dev/null
@@ -0,0 +1,43 @@
+#pragma once
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdbool.h>
+#include <stdlib.h>
+
+/*
+ * Return the offset, and the length of an ELF section with a given name in a given ELF file
+ */
+bool appimage_get_elf_section_offset_and_length(const char* fname, const char* section_name, unsigned long* offset, unsigned long* length);
+
+int appimage_print_hex(const char* fname, unsigned long offset, unsigned long length);
+int appimage_print_binary(const char* fname, unsigned long offset, unsigned long length);
+
+/*
+ * Creates hexadecimal representation of a byte array. Allocates a new char array (string) with the correct size that
+ * needs to be free()d.
+ */
+char* appimage_hexlify(const char* bytes, size_t numBytes);
+
+/*
+ * Calculate MD5 digest of AppImage file, skipping the signature and digest sections.
+ *
+ * The digest section must be skipped as the value calculated by this method is going to be embedded in it by default.
+ *
+ * The signature section must be skipped as the signature will not be available at the time this hash is calculated.
+ *
+ * The hash is _not_ compatible with tools like md5sum.
+ *
+ * You need to allocate a char array of at least 16 bytes (128 bit) and pass a reference to it as digest parameter.
+ * The function will set it to the raw digest, without any kind of termination. Please use appimage_hexlify() if you
+ * need a textual representation.
+ *
+ * Please beware that this calculation is only available for type 2 AppImages.
+ */
+bool appimage_type2_digest_md5(const char* fname, char* digest);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/include/appimage/core/AppImage.h b/include/appimage/core/AppImage.h
new file mode 100644 (file)
index 0000000..4c01506
--- /dev/null
@@ -0,0 +1,99 @@
+#pragma once
+
+// system
+#include <memory>
+#include <string>
+#include <vector>
+
+// local
+#include <appimage/core/AppImageFormat.h>
+#include <appimage/core/PayloadIterator.h>
+#include <appimage/core/exceptions.h>
+
+namespace appimage {
+    namespace core {
+        /**
+         * An object of class <appimage> represents an existent AppImage file. Provides readonly methods to
+         * access the AppImage information and contained files.
+         */
+        class AppImage {
+        public:
+            /**
+             * Open the AppImage at <path>.
+             * @param path
+             * @throw AppImageError if something goes wrong
+             */
+            explicit AppImage(const std::string& path);
+
+            /**
+             * Creates an AppImage instance from <other> AppImage
+             * @param other
+             */
+            AppImage(const AppImage& other);
+
+            /**
+             * Copy the <other> instance data into the current one.
+             * @param other
+             * @return
+             */
+            AppImage& operator=(const AppImage& other);
+
+            /**
+             * Default destructor.
+             *
+             * Required by `std::shared_ptr` to work properly.
+             */
+            virtual ~AppImage();
+
+            /**
+             * @return AppImage file path
+             */
+            const std::string& getPath() const;
+
+            /**
+             * Inspect the magic bytes of the file to guess the AppImage <FORMAT>
+             * @return AppImage <FORMAT>
+             */
+            AppImageFormat getFormat() const;
+
+            /**
+             * Inspect the magic bytes of the file to guess the AppImage <FORMAT>
+             * @return AppImage <FORMAT>
+             */
+            static AppImageFormat getFormat(const std::string& path);
+
+            /**
+             * Calculate the offset in the AppImage file where is located the payload file system.
+             *
+             * @return offset where the payload filesystem is located.
+             */
+            off_t getPayloadOffset() const;
+
+            /**
+             * Provides a one way iterator to traverse and access the files contained inside the AppImage.
+             * @return a files_iterator instance
+             * @throw AppImageError if something goes wrong
+             */
+            PayloadIterator files() const;
+
+            /**
+             * Compare this to <rhs>
+             * @param rhs
+             * @return true if both are equal, false otherwise
+             */
+            bool operator==(const AppImage& rhs) const;
+
+            /**
+             * Compare this to <rhs>
+             * @param rhs
+             * @return true if they are different, false otherwise
+             */
+            bool operator!=(const AppImage& rhs) const;
+
+        private:
+            class Private;
+
+            std::shared_ptr<Private> d;   // opaque pointer
+        };
+    }
+}
diff --git a/include/appimage/core/AppImageFormat.h b/include/appimage/core/AppImageFormat.h
new file mode 100644 (file)
index 0000000..a0e88be
--- /dev/null
@@ -0,0 +1,16 @@
+#pragma once
+
+namespace appimage {
+    namespace core {
+        /**
+         * The AppImage format determines how an AppImage is represented on disk. See the link below for more details
+         * https://github.com/AppImage/AppImageSpec/blob/master/draft.md#image-format
+         */
+        enum class AppImageFormat {
+            INVALID = -1,   // Not an AppImage file
+            LEGACY = 0,     // portable binaries that look and behave like AppImages but do not follow the standard
+            TYPE_1 = 1,      // https://github.com/AppImage/AppImageSpec/blob/master/draft.md#type-1-image-format
+            TYPE_2 = 2       // https://github.com/AppImage/AppImageSpec/blob/master/draft.md#type-2-image-format
+        };
+    }
+}
diff --git a/include/appimage/core/PayloadEntryType.h b/include/appimage/core/PayloadEntryType.h
new file mode 100644 (file)
index 0000000..1a278f8
--- /dev/null
@@ -0,0 +1,15 @@
+#pragma once
+
+namespace appimage {
+    namespace core {
+        /**
+         * Entry types known to the PayloadIterator
+         */
+        enum class PayloadEntryType {
+            UNKNOWN = -1,   // another kind of entry, could be a special file
+            REGULAR = 0,    // regular file
+            DIR = 1,        // directory
+            LINK = 2        // hard or symbolic link
+        };
+    }
+}
diff --git a/include/appimage/core/PayloadIterator.h b/include/appimage/core/PayloadIterator.h
new file mode 100644 (file)
index 0000000..3c39e5d
--- /dev/null
@@ -0,0 +1,135 @@
+#pragma once
+
+// system
+#include <memory>
+#include <iterator>
+
+// local
+#include <appimage/core/PayloadEntryType.h>
+
+namespace appimage {
+    namespace core {
+
+        // Forward declaration required because this file is included in AppImage.h
+        class AppImage;
+
+        /**
+         * A FilesIterator object provides a READONLY, SINGLE WAY, ONE PASS iterator over the files contained
+         * in the AppImage pointed by <path>. Abstracts the users from the AppImage file payload format.
+         *
+         * READONLY: files inside the AppImage cannot  be modified.
+         * SINGLE WAY: can't go backwards only forward.
+         * ONE PASS: A new instance is required to re-traverse the AppImage or re-read an entry.
+         */
+        class PayloadIterator : public std::iterator<std::input_iterator_tag, std::string> {
+        public:
+            /**
+             * Create a FilesIterator for <appImage>
+             * @param appImage
+             * @throw AppImageReadError in case of error
+             */
+            explicit PayloadIterator(const AppImage& appImage);
+
+            // Creating copies of this object is not allowed
+            PayloadIterator(PayloadIterator& other) = delete;
+
+            // Creating copies of this object is not allowed
+            PayloadIterator& operator=(PayloadIterator& other) = delete;
+
+            // Move constructor
+            PayloadIterator(PayloadIterator&& other) noexcept;
+
+            // Move assignment operator
+            PayloadIterator& operator=(PayloadIterator&& other) noexcept;
+
+            /**
+             * @return the type of the current file.
+             */
+            PayloadEntryType type();
+
+            /**
+             * @return file path pointed by the iterator
+             */
+            std::string path();
+
+            /**
+             * @return file link path if it's a LINK type file. Otherwise returns an empty string.
+             */
+            std::string linkTarget();
+
+            /**
+             * Extracts the file to the <target> path. Supports raw files, symlinks and directories.
+             * Parent target dir is created if not exists.
+             *
+             * IMPORTANT:
+             * - Due to implementation restrictions you can call read() or extractTo() in a given entry
+             *  only once. Additional call will throw a PayloadIteratorError.
+             *
+             *  @throw AppImageError if called on a PayloadEntry of UNKNOWN Type
+             * @param target
+             */
+            void extractTo(const std::string& target);
+
+            /**
+             * Read file content. Symbolic links will be resolved.
+             *
+             * IMPORTANT:
+             * - The returned istream becomes invalid after next is called, don't try to "reuse" it.
+             * - Due to implementation restrictions you can call read() or extractTo() a given entry
+             *  only once. Additional call will throw a PayloadIteratorError.
+             *
+             *  @throw AppImageError if called on a PayloadEntry of UNKNOWN Type
+             * @return file content stream
+             */
+            std::istream& read();
+
+            /**
+             * Compare this iterator to <other>.
+             * @param other
+             * @return true of both are equal, false otherwise
+             */
+            bool operator==(const PayloadIterator& other) const;
+
+            /**
+             * Compare this iterator to <other>.
+             * @param other
+             * @return true if are different, false otherwise
+             */
+            bool operator!=(const PayloadIterator& other) const;
+
+            /**
+             * @return file path pointed by the iterator
+             */
+            std::string operator*();
+
+            /**
+             * Move iterator to the next file.
+             * @return current file_iterator
+             */
+            PayloadIterator& operator++();
+
+            /**
+             * Represents the begin of the iterator. Will  always point to the current iterator.
+             * @return current file_iterator
+             */
+            PayloadIterator begin();
+
+            /**
+             * Represent the end of the iterator. Will  always point to an invalid iterator.
+             * @return invalid file_iterator
+             */
+            PayloadIterator end();
+
+        private:
+            class Private;
+
+            std::shared_ptr<Private> d;
+
+            /**
+             * Constructor used to create special representations of an iterator like the end state.
+             * @param private data of the iterator
+             */
+            explicit PayloadIterator(Private* d);
+        };
+    }
+}
diff --git a/include/appimage/core/exceptions.h b/include/appimage/core/exceptions.h
new file mode 100644 (file)
index 0000000..98fea17
--- /dev/null
@@ -0,0 +1,41 @@
+#pragma once
+
+// system
+#include <stdexcept>
+
+namespace appimage {
+    namespace core {
+        /**
+         * Generic Error that can be thrown by AppImage procedures.
+         */
+        class AppImageError : public std::runtime_error {
+        public:
+            explicit AppImageError(const std::string& what) : runtime_error(what) {}
+        };
+
+        /**
+         * Throw in case of missing files, insufficient permissions and other file system related
+         * errors.
+         */
+        class FileSystemError : public AppImageError {
+        public:
+            explicit FileSystemError(const std::string& what) : AppImageError(what) {}
+        };
+
+        /**
+         * Throw in case of failure in a read or write operation.
+         */
+        class IOError : public AppImageError {
+        public:
+            explicit IOError(const std::string& what) : AppImageError(what) {}
+        };
+
+        /**
+         * Throw in case of failure while iterating over the payload entries.
+         */
+        class PayloadIteratorError : public AppImageError {
+        public:
+            explicit PayloadIteratorError(const std::string& what) : AppImageError(what) {}
+        };
+    }
+};
diff --git a/include/appimage/desktop_integration/IntegrationManager.h b/include/appimage/desktop_integration/IntegrationManager.h
new file mode 100644 (file)
index 0000000..0edcd9c
--- /dev/null
@@ -0,0 +1,108 @@
+#pragma once
+
+// system
+#include <string>
+#include <memory>
+#include <iostream>
+
+// local
+#include <appimage/desktop_integration/exceptions.h>
+#include <appimage/core/AppImage.h>
+#include <appimage/config.h>
+
+namespace appimage {
+    namespace desktop_integration {
+        class IntegrationManager {
+        public:
+            /**
+             * Instantiate an Integration manager that will use as XDG_DATA_HOME the one pointed by the system
+             * configuration.
+             */
+            explicit IntegrationManager();
+
+            /**
+             * Instantiate an Integration manager that will use as XDG_DATA_HOME the one pointed by the <xdgDataHome>
+             */
+            explicit IntegrationManager(const std::string& xdgDataHome);
+
+            /**
+            * Creates an IntegrationManager instance from <other> IntegrationManager
+            * @param other
+            */
+            IntegrationManager(const IntegrationManager& other);
+
+            /**
+             * Copy the <other> instance data into the current one.
+             * @param other
+             * @return
+             */
+            IntegrationManager& operator=(const IntegrationManager& other);
+
+            virtual ~IntegrationManager();
+
+            /**
+             * @brief Register an AppImage in the system
+             *
+             * Extract the application main desktop entry, icons and mime type packages. Modifies their content to
+             * properly match the AppImage file location and deploy them into the use XDG_DATA_HOME appending a
+             * prefix to each file. Such prefix is composed as "<vendor id>_<appimage_path_md5>_<old_file_name>"
+             *
+             * @param appImage
+             */
+            void registerAppImage(const core::AppImage& appImage) const;
+
+            /**
+             * @brief Unregister an AppImage in the system
+             *
+             * Remove all files created by the registerAppImage function. The files are identified by matching the
+             * AppImageId contained in their names. The Id is made from the MD5 checksum of the <appImagePath>.
+             * @param appImagePath
+             */
+            void unregisterAppImage(const std::string& appImagePath) const;
+
+            /**
+             * @brief Check whether the AppImage pointed by <appImagePath> has been registered in the system.
+             *
+             * Explore XDG_DATA_HOME/applications looking for Destkop Entries files with a file name that matches
+             * the current AppImage Id (MD5 checksum of the <appImagePath>)
+             *
+             * @param appImagePath
+             * @return true if the AppImage is registered, false otherwise.
+             */
+            bool isARegisteredAppImage(const std::string& appImagePath) const;
+
+            /**
+             * @brief Check whether the author of an AppImage doesn't want it to be integrated.
+             *
+             * An AppImage is considered that shall not be integrated if fulfill any of the following conditions:
+             *  - The AppImage's desktop file has set X-AppImage-Integrate=false.
+             *  - The AppImage's desktop file has set Terminal=true.
+             *
+             * @param appImage
+             * @return false if any of the conditions are meet, true otherwise
+             */
+            bool shallAppImageBeRegistered(const core::AppImage& appImage) const;
+
+#ifdef LIBAPPIMAGE_THUMBNAILER_ENABLED
+            /**
+             * @brief Generate thumbnails according to the FreeDesktop Thumbnail Managing Standard
+             * See: https://specifications.freedesktop.org/thumbnail-spec/0.8.0/index.html
+             * @param appImage
+             */
+            void generateThumbnails(const core::AppImage& appImage) const;
+
+            /**
+             * @brief Remove thumbnails according to the FreeDesktop Thumbnail Managing Standard
+             * See: https://specifications.freedesktop.org/thumbnail-spec/0.8.0/index.html
+             * @param appImagePath
+             */
+            void removeThumbnails(const std::string& appImagePath) const;
+
+#endif
+
+        private:
+            class Private;
+            std::shared_ptr<Private> d;   // opaque pointer
+        };
+    }
+}
diff --git a/include/appimage/desktop_integration/exceptions.h b/include/appimage/desktop_integration/exceptions.h
new file mode 100644 (file)
index 0000000..b163698
--- /dev/null
@@ -0,0 +1,18 @@
+#pragma once
+
+// local
+#include <appimage/core/exceptions.h>
+
+namespace appimage {
+    namespace desktop_integration {
+
+        /**
+         * Throw in case of failure while performing a given desktop integration operation.
+         * For example when a DesktopEntry is missing or malformed.
+         */
+        class DesktopIntegrationError : public core::AppImageError {
+        public:
+            explicit DesktopIntegrationError(const std::string& what) : core::AppImageError(what) {}
+        };
+    }
+}
diff --git a/include/appimage/utils/ResourcesExtractor.h b/include/appimage/utils/ResourcesExtractor.h
new file mode 100644 (file)
index 0000000..6afaeaa
--- /dev/null
@@ -0,0 +1,87 @@
+#pragma once
+
+// system
+#include <map>
+#include <string>
+#include <vector>
+#include <memory>
+
+// libraries
+#include <appimage/core/AppImage.h>
+
+namespace appimage {
+    namespace utils {
+        /**
+         * Allows to identify and extract the resources (files) required to integrate an AppImage into the
+         * desktop environment in an effective way.
+         *
+         * Using the `PayloadIterator::read` method on symlinks is not reliable as it's not supported on
+         * AppImages of type 1 (blame on `libarchive`). To overcome this limitation two iterations over the
+         * AppImage will be performed. One to resolve all the links entries and other to actually extract
+         * the resources.
+         */
+        class ResourcesExtractor {
+        public:
+            explicit ResourcesExtractor(const core::AppImage& appImage);
+
+            /**
+             * @brief Read an entry into memory, if the entry is a link it will be resolved.
+             * @return entry data
+             * @throw PayloadIteratorError if the entry doesn't exists
+             */
+            std::vector<char> extract(const std::string& path) const;
+
+            /**
+             * @brief Read each entry into memory, if the entry is a link it will be resolved.
+             * @return entries data
+             * @throw PayloadIteratorError if some entry doesn't exists
+             */
+            std::map<std::string, std::vector<char>> extract(const std::vector<std::string>& paths) const;
+
+            /**
+             * Extract entries listed in 'first' member of the <targetsMap> iterator to the 'second' member
+             * of the <targetsMap> iterator. Will resolve links to regular files.
+             *
+             * @param targetsMap
+             */
+            void extractTo(const std::map<std::string, std::string>& targetsMap) const;
+
+            /**
+             * @brief Read an entry into a std::string, if the entry is a link it will be resolved.
+             * Should only be used in text files.
+             *
+             * @return entry data
+             * @throw PayloadIteratorError if the entry doesn't exists
+             */
+            std::string extractText(const std::string& path) const;
+
+            /**
+             * @return path to the main desktop entry of the AppImage
+             */
+            std::string getDesktopEntryPath() const;
+
+            /**
+             * Icons are expected to be located in "usr/share/icons/" according to the FreeDesktop
+             * Icon Theme Specification. This method look for entries in that path whose file name
+             * matches to the iconName
+             *
+             * @param iconName
+             * @return list of the icon entries paths
+             */
+            std::vector<std::string> getIconFilePaths(const std::string& iconName) const;
+
+            /**
+             * Mime-Type packages are xml files located usr/share/mime/packages according to the
+             * Shared MIME-info Database specification.
+             *
+             * @param iconName
+             * @return Mime-Type packages entries paths
+             */
+            std::vector<std::string> getMimeTypePackagesPaths() const;
+
+        private:
+            class Priv;
+            std::shared_ptr<Priv> d;
+        };
+    }
+}
diff --git a/include/appimage/utils/logging.h b/include/appimage/utils/logging.h
new file mode 100644 (file)
index 0000000..8bdb9ac
--- /dev/null
@@ -0,0 +1,22 @@
+#pragma once
+
+#include <string>
+#include <functional>
+
+namespace appimage {
+    namespace utils {
+        enum class LogLevel {
+            DEBUG = 0, INFO, WARNING, ERROR
+        };
+
+        typedef std::function<void(const utils::LogLevel& level, const std::string& message)> log_callback_t;
+
+        /**
+         * @brief Set a custom logging function.
+         * Allows to capture the libappimage log messages.
+         *
+         * @param logging function callback
+         */
+        void setLoggerCallback(const log_callback_t& callback);
+    }
+}
diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt
new file mode 100644 (file)
index 0000000..4daa663
--- /dev/null
@@ -0,0 +1,13 @@
+cmake_minimum_required(VERSION 3.0)
+
+include(CTest)
+
+if(BUILD_TESTING)
+    if(NOT TARGET gtest)
+        if(IS_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/gtest)
+            add_subdirectory(gtest EXCLUDE_FROM_ALL)
+        else()
+            message(FATAL_ERROR "gtest submodule not found; please call git submodule update --init or disable the unit tests using -DBUILD_TESTING=OFF")
+        endif()
+    endif()
+endif()
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
new file mode 100644 (file)
index 0000000..9737205
--- /dev/null
@@ -0,0 +1,56 @@
+# required for pkg-config to create PkgConfig::<prefix> imported library targets
+cmake_minimum_required(VERSION 3.6)
+
+add_subdirectory(xdg-basedir)
+add_subdirectory(libappimage_hashlib)
+add_subdirectory(libappimage_shared)
+if(NOT LIBAPPIMAGE_SHARED_ONLY)
+    add_subdirectory(libappimage)
+endif()
+
+# Export the package for use from the build-tree
+# (this registers the build-tree with a global CMake-registry)
+export(PACKAGE libappimage)
+
+include(CMakePackageConfigHelpers)
+
+# Create the AppImageConfig.cmake and AppImageConfigVersion files
+configure_package_config_file(
+    "${PROJECT_SOURCE_DIR}/cmake/libappimageConfig.cmake.in"
+    "${PROJECT_BINARY_DIR}/${CMAKE_FILES_DIRECTORY}/libappimageConfig.cmake"
+    INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/libappimage
+)
+# ... for both
+configure_file(
+    "${PROJECT_SOURCE_DIR}/cmake/libappimageConfigVersion.cmake.in"
+    "${PROJECT_BINARY_DIR}/${CMAKE_FILES_DIRECTORY}/libappimageConfigVersion.cmake"
+    @ONLY
+)
+
+# Install the AppImageConfig.cmake and AppImageConfigVersion.cmake
+install(
+    FILES
+    "${PROJECT_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/libappimageConfig.cmake"
+    "${PROJECT_BINARY_DIR}/${CMAKE_FILES_DIRECTORY}/libappimageConfigVersion.cmake"
+    "${PROJECT_BINARY_DIR}/cmake/scripts.cmake"
+    "${PROJECT_BINARY_DIR}/cmake/imported_dependencies.cmake"
+    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/libappimage
+    COMPONENT libappimage-dev
+)
+
+# Install the export set for use with the install-tree
+install(
+    EXPORT libappimageTargets
+    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/libappimage
+    COMPONENT libappimage-dev
+)
+
+if(NOT LIBAPPIMAGE_SHARED_ONLY)
+    # pkg-config
+    configure_file("${PROJECT_SOURCE_DIR}/cmake/libappimage.pc.in" "${PROJECT_BINARY_DIR}/libappimage.pc" @ONLY)
+
+    install(FILES "${PROJECT_BINARY_DIR}/libappimage.pc"
+        DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig
+        COMPONENT libappimage-dev
+    )
+endif()
diff --git a/src/libappimage/CMakeLists.txt b/src/libappimage/CMakeLists.txt
new file mode 100644 (file)
index 0000000..6b24f58
--- /dev/null
@@ -0,0 +1,89 @@
+cmake_minimum_required(VERSION 3.2)
+
+add_subdirectory(core)
+add_subdirectory(utils)
+
+set(
+    libappimage_sources
+    libappimage.c
+    libappimage.cpp
+    $<TARGET_OBJECTS:core>
+    $<TARGET_OBJECTS:appimage_utils>
+)
+if(LIBAPPIMAGE_DESKTOP_INTEGRATION_ENABLED)
+    add_subdirectory(desktop_integration)
+    list(APPEND libappimage_sources "$<TARGET_OBJECTS:appimage_desktop_integration>")
+endif()
+
+add_library(libappimage_static STATIC ${libappimage_sources})
+add_library(libappimage SHARED ${libappimage_sources} libappimage_legacy.cpp)
+
+configure_file(config.h.in ${PROJECT_BINARY_DIR}/generated-headers/appimage/config.h)
+
+foreach(target libappimage libappimage_static)
+    configure_libappimage_module(${target})
+    target_link_libraries(
+        ${target}
+        PRIVATE libarchive
+        PRIVATE xdg-basedir
+        PRIVATE XdgUtils::DesktopEntry
+        PRIVATE XdgUtils::BaseDir
+        PRIVATE libappimage_hashlib
+        # not linking publicly to squashfuse as headers are not needed when using libappimage
+        # unit tests etc., which use squashfuse directly, must link to it explicitly
+        PRIVATE libsquashfuse
+        PRIVATE xz
+        PRIVATE Boost::filesystem
+        PUBLIC libappimage_shared
+        PUBLIC pthread
+        PRIVATE libgio
+        PRIVATE libzlib
+        PUBLIC libcairo
+        PUBLIC librsvg
+        PUBLIC dl
+    )
+    message(STATUS "IMAGE_MANIPULATION_BACKEND_LIBS ${IMAGE_MANIPULATION_BACKEND_LIBS}")
+    if(LIBAPPIMAGE_STANDALONE)
+        target_link_libraries(${target} PRIVATE -static-libgcc -static-libstdc++)
+    endif()
+
+    target_include_directories(
+        ${target}
+        PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
+        PUBLIC $<BUILD_INTERFACE:${PROJECT_BINARY_DIR}/generated-headers>
+    )
+
+    set_property(TARGET libappimage PROPERTY PUBLIC_HEADER ${libappimage_public_header})
+
+    set_property(TARGET libappimage PROPERTY VERSION ${libappimage_VERSION})
+    set_property(TARGET libappimage PROPERTY SOVERSION ${libappimage_SOVERSION})
+endforeach()
+
+
+# install libappimage
+install(
+    TARGETS libappimage
+    EXPORT libappimageTargets
+    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT libappimage
+    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT libappimage
+    PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/appimage COMPONENT libappimage-dev
+)
+
+# install public headers
+install(
+    DIRECTORY ${PROJECT_SOURCE_DIR}/include/appimage/
+    DESTINATION include/appimage
+    COMPONENT libappimage-dev
+)
+
+install(
+    DIRECTORY ${PROJECT_BINARY_DIR}/generated-headers/appimage/
+    DESTINATION include/appimage
+    COMPONENT libappimage-dev
+)
+
+# Add all targets to the build-tree export set
+export(
+    TARGETS libappimage libappimage_shared libappimage_hashlib
+    FILE "${PROJECT_BINARY_DIR}/${CMAKE_FILES_DIRECTORY}/libappimageTargets.cmake"
+)
diff --git a/src/libappimage/appimage_handler.h b/src/libappimage/appimage_handler.h
new file mode 100644 (file)
index 0000000..ceb3f0d
--- /dev/null
@@ -0,0 +1,53 @@
+#pragma once
+
+// system includes
+#include <glib.h>
+#include <stdbool.h>
+
+/* AppImage generic handler calback to be used in algorithms */
+typedef void (*traverse_cb)(void* handler, void* entry_data, void* user_data);
+
+/* AppImage generic handler to be used in algorithms */
+struct appimage_handler {
+    const gchar* path;
+
+    char* (*get_file_name)(struct appimage_handler* handler, void* entry);
+
+    void (*extract_file)(struct appimage_handler* handler, void* entry, const char* target);
+
+    bool (*read_file_into_new_buffer)(struct appimage_handler* handler, void* entry, char** buffer, unsigned long* buffer_size);
+
+    char* (*get_file_link)(struct appimage_handler* handler, void* entry);
+
+    void (*traverse)(struct appimage_handler* handler, traverse_cb command, void* user_data);
+
+    void* cache;
+    bool is_open;
+
+    // for debugging purposes
+    int type;
+} typedef appimage_handler;
+
+/*
+ * appimage_handler functions
+ */
+
+// constructor
+appimage_handler create_appimage_handler(const char* const path);
+
+/*
+ * utility functions
+ */
+bool is_handler_valid(const appimage_handler* handler);
+
+void mk_base_dir(const char *target);
+
+/*
+ * dummy fallback callbacks
+ */
+void dummy_traverse_func(appimage_handler* handler, traverse_cb command, void* data);
+
+char* dummy_get_file_name(appimage_handler* handler, void* data);
+
+void dummy_extract_file(struct appimage_handler* handler, void* data, char* target);
+
diff --git a/src/libappimage/config.h.in b/src/libappimage/config.h.in
new file mode 100644 (file)
index 0000000..83be220
--- /dev/null
@@ -0,0 +1,15 @@
+#pragma once
+
+// LibAppImage Version
+
+#define LIBAPPIMAGE_VERSION_MAJOR @V_MAJOR@
+#define LIBAPPIMAGE_VERSION_MINOR @V_MINOR@
+#define LIBAPPIMAGE_VERSION_PATCH @V_PATCH@
+#define LIBAPPIMAGE_VERSION_SUFFIX @V_SUFFIX@
+
+#define LIBAPPIMAGE_VERSION "@libappimage_VERSION@"
+
+// Features
+
+#cmakedefine LIBAPPIMAGE_DESKTOP_INTEGRATION_ENABLED
+#cmakedefine LIBAPPIMAGE_THUMBNAILER_ENABLED
diff --git a/src/libappimage/core/AppImage.cpp b/src/libappimage/core/AppImage.cpp
new file mode 100644 (file)
index 0000000..ce0fb01
--- /dev/null
@@ -0,0 +1,95 @@
+// system
+#include <iostream>
+#include <algorithm>
+
+
+// local
+#include "appimage/core/AppImage.h"
+#include "utils/MagicBytesChecker.h"
+#include "utils/ElfFile.h"
+
+namespace appimage {
+    namespace core {
+
+        /**
+         * Implementation of the opaque pointer patter for the appimage class
+         * see https://en.wikipedia.org/wiki/Opaque_pointer
+         */
+        class AppImage::Private {
+        public:
+            std::string path;
+            AppImageFormat format = AppImageFormat::INVALID;
+
+            explicit Private(const std::string& path);
+
+            static AppImageFormat getFormat(const std::string& path);
+
+        };
+
+        AppImage::AppImage(const std::string& path) : d(new Private(path)) {
+        }
+
+        const std::string& AppImage::getPath() const {
+            return d->path;
+        }
+
+        AppImageFormat AppImage::getFormat() const {
+            return d->format;
+        }
+
+        AppImageFormat AppImage::getFormat(const std::string& path) {
+            return Private::getFormat(path);
+        }
+
+        AppImage::Private::Private(const std::string& path) : path(path) {
+            format = getFormat(path);
+
+            if (format == AppImageFormat::INVALID)
+                throw core::AppImageError("Unknown AppImage format: " + path); // FIXME: This should not be an error, and should not be printed unless in debug mode
+        }
+
+        AppImageFormat AppImage::Private::getFormat(const std::string& path) {
+            utils::MagicBytesChecker magicBytesChecker(path);
+
+            if (!magicBytesChecker.hasElfSignature())
+                return AppImageFormat::INVALID;
+
+            if (magicBytesChecker.hasAppImageType1Signature())
+                return AppImageFormat::TYPE_1;
+
+            if (magicBytesChecker.hasAppImageType2Signature())
+                return AppImageFormat::TYPE_2;
+
+            if (magicBytesChecker.hasIso9660Signature()) {
+                std::cerr << "WARNING: " << path << " seems to be a Type 1 AppImage without magic bytes." << std::endl;
+                return AppImageFormat::TYPE_1;
+            }
+
+            return AppImageFormat::INVALID;
+        }
+
+        AppImage::~AppImage() = default;
+
+        PayloadIterator AppImage::files() const {
+            return PayloadIterator(*this);
+        }
+
+        off_t AppImage::getPayloadOffset() const {
+            utils::ElfFile elf(d->path);
+
+            return elf.getSize();
+        }
+
+        bool AppImage::operator==(const AppImage& rhs) const {
+            return d == rhs.d;
+        }
+
+        bool AppImage::operator!=(const AppImage& rhs) const {
+            return !(rhs == *this);
+        }
+
+        AppImage& AppImage::operator=(const AppImage& other) = default;
+
+        AppImage::AppImage(const AppImage& other) = default;
+    }
+}
diff --git a/src/libappimage/core/CMakeLists.txt b/src/libappimage/core/CMakeLists.txt
new file mode 100644 (file)
index 0000000..5737843
--- /dev/null
@@ -0,0 +1,24 @@
+add_library(
+    core OBJECT
+    AppImage.cpp
+    Traversal.h
+    Traversal.cpp
+    PayloadIterator.cpp
+    impl/TraversalType1.cpp
+    impl/TraversalType2.cpp
+    impl/StreambufType1.cpp
+    impl/StreambufType2.cpp
+)
+
+target_include_directories(
+    core
+    PRIVATE $<TARGET_PROPERTY:Boost::filesystem,INTERFACE_INCLUDE_DIRECTORIES>
+    PRIVATE $<TARGET_PROPERTY:libappimage_hashlib,INTERFACE_INCLUDE_DIRECTORIES>
+    PRIVATE $<TARGET_PROPERTY:XdgUtils::DesktopEntry,INTERFACE_INCLUDE_DIRECTORIES>
+    PRIVATE $<TARGET_PROPERTY:libarchive,INTERFACE_INCLUDE_DIRECTORIES>
+    PRIVATE $<TARGET_PROPERTY:libsquashfuse,INTERFACE_INCLUDE_DIRECTORIES>
+    PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/..
+)
+
+configure_libappimage_module(core)
+add_dependencies(core Boost::filesystem libappimage_hashlib XdgUtils::DesktopEntry libarchive libsquashfuse)
diff --git a/src/libappimage/core/PayloadIterator.cpp b/src/libappimage/core/PayloadIterator.cpp
new file mode 100644 (file)
index 0000000..3fbb988
--- /dev/null
@@ -0,0 +1,175 @@
+// system
+#include <sstream>
+
+// local
+#include <appimage/core/PayloadIterator.h>
+#include <appimage/core/AppImage.h>
+#include "core/impl/TraversalType1.h"
+#include "core/impl/TraversalType2.h"
+
+namespace appimage {
+    namespace core {
+
+        /**
+         * @brief Representation of the private state of the iterator.
+         *
+         * A Traversal class is used to traverse the files and directories inside the AppImage payload. The required
+         * Traversal derivative is instantiated on demand by the constructor.
+         *
+         * The "end state" of the iterator is represented by a nullptr traversal.
+         *
+         * The major part of this class methods are proxies over the Traversal class. Therefore they behave the same. If
+         * the traversal reach the "end state" those methods will return a default value to keep the integrity of the
+         * iterator.
+         */
+        class PayloadIterator::Private {
+            AppImage appImage;
+
+            // to be used by the read method when the end of the traversal is reached
+            std::stringstream emptyStream;
+
+            // Real Traversal implementation
+            std::shared_ptr<Traversal> traversal;
+
+            // flags whether a the current entry contents has been read or not
+            bool entryDataConsumed = false;
+        public:
+            /**
+             * Initialized the Private class with required traversal derivative if <atEnd> is false.
+             * @param appImage
+             * @param atEnd determine if a end state iterator should be created
+             */
+            explicit Private(const AppImage& appImage, bool atEnd = false) : appImage(appImage) {
+                // only initialize if the iterator is not in the "end state"
+                if (!atEnd) {
+                    switch (appImage.getFormat()) {
+                        case AppImageFormat::TYPE_1:
+                            traversal = std::shared_ptr<Traversal>(new impl::TraversalType1(appImage.getPath()));
+                            break;
+                        case AppImageFormat::TYPE_2:
+                            traversal = std::shared_ptr<Traversal>(new impl::TraversalType2(appImage.getPath()));
+                            break;
+                        default:
+                            break;
+                    }
+                }
+            }
+
+            // Creating copies of this object is not allowed
+            Private(Private& other) = delete;
+
+            // Creating copies of this object is not allowed
+            Private& operator=(Private& other) = delete;
+
+            // Move constructor
+            Private(PayloadIterator::Private&& other) noexcept : appImage(other.appImage),
+                                                                 traversal(other.traversal) {}
+
+            // Move assignment operator
+            PayloadIterator::Private& operator=(PayloadIterator::Private&& other) noexcept {
+                appImage = other.appImage;
+                traversal = other.traversal;
+                return *this;
+            }
+
+            /**
+             * Compare Private data according to the AppImage they point to and to the traversal instance.
+             * @param rhs
+             * @return
+             */
+            bool operator==(const PayloadIterator::Private& rhs) const {
+                return appImage == rhs.appImage &&
+                       traversal == rhs.traversal;
+            }
+
+            bool operator!=(const PayloadIterator::Private& rhs) const {
+                return !(rhs == *this);
+            }
+
+            bool isCompleted() { return traversal == nullptr; }
+
+            void next() {
+                // move forward only if we haven't reached the end
+                if (traversal) {
+                    traversal->next();
+
+                    // unset entryDataConsumed flag
+                    entryDataConsumed = false;
+
+                    // release traversal instance when completed in order to match with the "end state"
+                    if (traversal->isCompleted())
+                        traversal.reset();
+                }
+            }
+
+            PayloadEntryType type() { return isCompleted() ? PayloadEntryType::UNKNOWN : traversal->getEntryType(); }
+
+            std::string entryName() { return isCompleted() ? std::string() : traversal->getEntryPath(); }
+
+            std::string entryLink() { return isCompleted() ? std::string() : traversal->getEntryLinkTarget(); }
+
+            void extractTo(const std::string& target) {
+                // Enforce ONE PASS restriction
+                if (entryDataConsumed)
+                    throw PayloadIteratorError("Entry data consumed");
+                else
+                    entryDataConsumed = true;
+
+                if (!isCompleted()) traversal->extract(target);
+            }
+
+            std::istream& read() {
+                // Enforce ONE PASS restriction
+                if (entryDataConsumed)
+                    throw PayloadIteratorError("Entry data consumed");
+                else
+                    entryDataConsumed = true;
+
+                return isCompleted() ? emptyStream : traversal->read();
+            }
+
+            Private* beginState() { return new Private(appImage); }
+
+            Private* endState() { return new Private(appImage, true); }
+        };
+
+        PayloadIterator::PayloadIterator(const AppImage& appImage) : d(new Private(appImage)) {}
+
+        PayloadIterator::PayloadIterator(PayloadIterator&& other) noexcept { d = other.d; }
+
+        PayloadIterator& PayloadIterator::operator=(PayloadIterator&& other) noexcept {
+            d = other.d;
+            return *this;
+        }
+
+        PayloadEntryType PayloadIterator::type() { return d->type(); }
+
+        std::string PayloadIterator::path() { return d->entryName(); }
+
+        std::string PayloadIterator::linkTarget() { return d->entryLink(); }
+
+        void PayloadIterator::extractTo(const std::string& target) { d->extractTo(target); }
+
+        std::istream& PayloadIterator::read() { return d->read(); }
+
+        std::string PayloadIterator::operator*() { return d->entryName(); }
+
+        PayloadIterator& PayloadIterator::operator++() {
+            // move to the next entry in the traversal
+            d->next();
+
+            return *this;
+        }
+
+        PayloadIterator PayloadIterator::begin() { return PayloadIterator(d->beginState()); }
+
+        PayloadIterator PayloadIterator::end() { return PayloadIterator(d->endState()); }
+
+        PayloadIterator::PayloadIterator(PayloadIterator::Private* d) : d(d) {}
+
+        bool PayloadIterator::operator==(const PayloadIterator& other) const { return *d == *(other.d); }
+
+        bool PayloadIterator::operator!=(const PayloadIterator& other) const { return !(other == *this); }
+
+    }
+}
diff --git a/src/libappimage/core/Traversal.cpp b/src/libappimage/core/Traversal.cpp
new file mode 100644 (file)
index 0000000..919adb7
--- /dev/null
@@ -0,0 +1,16 @@
+
+// local
+#include "Traversal.h"
+namespace appimage {
+    namespace core {
+        bool Traversal::operator==(const Traversal& rhs) const {
+            return getEntryPath() == rhs.getEntryPath() &&
+                   getEntryType() == rhs.getEntryType() &&
+                   getEntryLinkTarget() == rhs.getEntryLinkTarget();
+        }
+
+        bool Traversal::operator!=(const Traversal& rhs) const {
+            return !operator==(rhs);
+        }
+    }
+}
diff --git a/src/libappimage/core/Traversal.h b/src/libappimage/core/Traversal.h
new file mode 100644 (file)
index 0000000..4f1c933
--- /dev/null
@@ -0,0 +1,78 @@
+#pragma once
+
+// system
+#include <string>
+
+// local
+#include <appimage/core/PayloadEntryType.h>
+
+namespace appimage {
+    namespace core {
+        /**
+         * Abstract representation of an AppImage traversal operation. Serves as extension point for the
+         * <files_iterator> class. Has the following restrictions:
+         *
+         * - READONLY: files inside the AppImage cannot  be modified.
+         * - SINGLE WAY: can't go backwards only forward.
+         * - ONE PASS: A new instance is required to re-traverse the AppImage.
+         * - NO ORDER: There is no warranty that the traversal will follow a given order.
+         */
+        class Traversal {
+        public:
+            /**
+             * Move to the next entry in the AppImage.
+             */
+            virtual void next() = 0;
+
+            /**
+             * @return true if the end of the traversal was reached, false otherwise
+             */
+            virtual bool isCompleted() const = 0;
+
+            /**
+             * @return name of the file entry inside the AppImage
+             */
+            virtual std::string getEntryPath() const = 0;
+
+            /**
+             * @return the target link of the current entry if it's of type LINK. Otherwise return an empty string.
+             */
+            virtual std::string getEntryLinkTarget() const = 0;
+
+            /**
+             * @return the type of the current entry.
+             */
+            virtual PayloadEntryType getEntryType() const = 0;
+
+            /**
+             * Extracts the file to the <target> path. Supports raw files, symlinks and directories.
+             * Parent target dir is created if not exists.
+             * @param target path the file should be extracted
+             */
+            virtual void extract(const std::string& target) = 0;
+
+            /**
+             * Read file content.
+             *
+             * The returned istream is bind to the current entry and it becomes invalid every time next()
+             * is called. That's why it's a reference.
+             * @return file content stream
+             */
+            virtual std::istream& read() = 0;
+
+            /**
+             * Compare this to <rhs>
+             * @param rhs
+             * @return true if both are equal, false otherwise
+             */
+            bool operator==(const Traversal& rhs) const;
+
+            /**
+             * Compare this to <rhs>
+             * @param rhs
+             * @return true if they are different, false otherwise
+             */
+            bool operator!=(const Traversal& rhs) const;
+        };
+    }
+}
diff --git a/src/libappimage/core/impl/PayloadIStream.h b/src/libappimage/core/impl/PayloadIStream.h
new file mode 100644 (file)
index 0000000..01f3ef5
--- /dev/null
@@ -0,0 +1,30 @@
+#pragma once
+// system
+#include <istream>
+#include <memory>
+
+namespace appimage {
+    namespace core {
+        namespace impl {
+            /**
+             * @brief Convenience wrapper around std::streambuf to allow the creation of std::istream instances from the files
+             * contained inside a given AppImage.
+             *
+             * @related traversal.h
+             */
+            class PayloadIStream : public std::istream {
+            public:
+                friend class TraversalType1;
+                friend class TraversalType2;
+
+                PayloadIStream() = default;
+
+                // Creating copies of this object is not allowed
+                PayloadIStream(PayloadIStream& other) = delete;
+
+                // Creating copies of this object is not allowed
+                PayloadIStream& operator=(PayloadIStream& other) = delete;
+            };
+        }
+    }
+}
diff --git a/src/libappimage/core/impl/StreambufType1.cpp b/src/libappimage/core/impl/StreambufType1.cpp
new file mode 100644 (file)
index 0000000..8ceec88
--- /dev/null
@@ -0,0 +1,42 @@
+// local
+#include <appimage/core/exceptions.h>
+#include "StreambufType1.h"
+
+using namespace appimage::core::impl;
+
+int StreambufType1::underflow() {
+    // Read line from original source
+    auto bytesRead = archive_read_data(a, buffer.data(), size);
+
+    // In case of error a value lower than 0 is returned
+    if (bytesRead < 0)
+        throw IOError(archive_error_string(a));
+
+    // notify eof if nothing
+    if (bytesRead == 0) return traits_type::eof();
+
+    // Update streambuf read pointers see <setg> doc
+    setg(buffer.data(), buffer.data(), buffer.data() + bytesRead);
+
+    // return the first char
+    return traits_type::to_int_type(*gptr());
+}
+
+StreambufType1::StreambufType1(archive* a, unsigned long size) : a(a), size(size), buffer(size) {}
+
+StreambufType1::StreambufType1(StreambufType1&& other) noexcept
+    : a(other.a), size(other.size), buffer(std::move(other.buffer)) {
+    // Reset the three read area pointers
+    setg(other._M_in_beg, other._M_in_cur, other._M_in_end);
+}
+
+StreambufType1& StreambufType1::operator=(StreambufType1&& other) noexcept {
+    a = other.a;
+    size = other.size;
+    buffer = std::move(other.buffer);
+
+    // Reset the three read area pointers
+    setg(other._M_in_beg, other._M_in_cur, other._M_in_end);
+
+    return *this;
+}
diff --git a/src/libappimage/core/impl/StreambufType1.h b/src/libappimage/core/impl/StreambufType1.h
new file mode 100644 (file)
index 0000000..b724306
--- /dev/null
@@ -0,0 +1,56 @@
+#pragma once
+
+// system
+#include <vector>
+#include <streambuf>
+
+// libraries
+#include <archive.h>
+
+namespace appimage {
+    namespace core {
+        namespace impl {
+            /**
+             * Provides a streambuf implementation for reading type 1 AppImages
+             * by means of libarchive.
+             *
+             * For more details about streambuf see https://gcc.gnu.org/onlinedocs/libstdc++/manual/streambufs.html
+             */
+            class StreambufType1 : public std::streambuf {
+            public:
+                /**
+                 * Create an streambuf_type_1 object from an archive <a> pointer
+                 * with a buffer size of <size>
+                 * @param a opened archive struct from libarchive
+                 * @param size buffer size
+                 */
+                StreambufType1(archive* a, unsigned long size);
+
+                // Creating copies of this object is not allowed
+                StreambufType1(StreambufType1& other) = delete;
+
+                // Creating copies of this object is not allowed
+                StreambufType1& operator=(StreambufType1& other) = delete;
+
+                // Move constructor
+                StreambufType1(StreambufType1&& other) noexcept;
+
+                // Move assignment operator
+                StreambufType1& operator=(StreambufType1&& other) noexcept;
+
+            protected:
+                /**
+                 * @brief  Fetches more data from the controlled sequence.
+                 * See parenth method documentation.
+                 * @return e first character from the <em>pending sequence</em>.
+                 */
+                int underflow() override;
+
+            private:
+                unsigned long size;
+                std::vector<char> buffer;
+                struct archive* a = {nullptr};
+            };
+        }
+    }
+}
diff --git a/src/libappimage/core/impl/StreambufType2.cpp b/src/libappimage/core/impl/StreambufType2.cpp
new file mode 100644 (file)
index 0000000..ceed7fb
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * NOTE ON SQUASHFUSE:
+ * It wasn't designed originally as a library and its headers are somehow broken.
+ * Therefore they must be kept confined.
+ *
+ * keep squashfuse includes on top to avoid _POSIX_C_SOURCE redefinition warning
+*/
+extern "C" {
+#include <squashfuse.h>
+#include <squashfs_fs.h>
+
+#include <sys/stat.h>
+}
+
+// local
+#include "appimage/core/exceptions.h"
+#include "StreambufType2.h"
+
+using namespace appimage::core::impl;
+
+StreambufType2::StreambufType2(sqfs* fs, sqfs_inode* inode, unsigned long size)
+    : fs(fs), inode(inode), buffer(size) {
+}
+
+StreambufType2::StreambufType2(StreambufType2&& other) noexcept
+    : fs(other.fs), inode(other.inode), buffer(std::move(other.buffer)) {
+
+    // Reset the three read area pointers
+    setg(other._M_in_beg, other._M_in_cur, other._M_in_end);
+}
+
+StreambufType2& StreambufType2::operator=(StreambufType2&& other) noexcept {
+    fs = other.fs;
+    inode = other.inode;
+    buffer = std::move(other.buffer);
+
+    // Reset the three read area pointers
+    setg(other._M_in_beg, other._M_in_cur, other._M_in_end);
+    return *this;
+}
+
+int StreambufType2::underflow() {
+    // notify eof if the whole file was read
+    if (bytes_already_read >= inode->xtra.reg.file_size) return traits_type::eof();
+
+    // read next data chunk
+    sqfs_off_t bytes_at_a_time = buffer.size();
+    if (sqfs_read_range(fs, inode, (sqfs_off_t) bytes_already_read, &bytes_at_a_time, buffer.data()))
+        throw IOError("sqfs_read_range error");
+
+    bytes_already_read += bytes_at_a_time;
+
+    // Update streambuf read pointers see <setg> doc
+    setg(buffer.data(), buffer.data(), buffer.data() + bytes_at_a_time);
+
+    // return the first char
+    return traits_type::to_int_type(*gptr());
+}
diff --git a/src/libappimage/core/impl/StreambufType2.h b/src/libappimage/core/impl/StreambufType2.h
new file mode 100644 (file)
index 0000000..e437985
--- /dev/null
@@ -0,0 +1,62 @@
+#pragma once
+
+#include <memory>
+#include <streambuf>
+#include <vector>
+
+extern "C" {
+#include <squashfuse.h>
+#include <squashfs_fs.h>
+}
+
+
+namespace appimage {
+    namespace core {
+        namespace impl {
+            /**
+             * Provides a streambuf implementation for reading type 2 AppImages
+             * by means of squashfuse.
+             *
+             * For more details about streambuf see https://gcc.gnu.org/onlinedocs/libstdc++/manual/streambufs.html
+             */
+            class StreambufType2 : public std::streambuf {
+            public:
+                /**
+                 * Create an streambuf_type_2 object for reading the file pointed by <inode> at <fs>
+                 * of size <size>
+                 * @param fs
+                 * @param inode
+                 * @param size
+                 */
+                StreambufType2(sqfs* fs, sqfs_inode* inode, unsigned long size);
+
+                // Creating copies of this object is not allowed
+                StreambufType2(StreambufType2& other) = delete;
+
+                // Creating copies of this object is not allowed
+                StreambufType2& operator=(StreambufType2& other) = delete;
+
+                // Move constructor
+                StreambufType2(StreambufType2&& other) noexcept;
+
+                // Move assignment operator
+                StreambufType2& operator=(StreambufType2&& other) noexcept;
+
+            protected:
+                /**
+                 * @brief  Fetches more data from the controlled sequence.
+                 * See the superclass method documentation.
+                 * @return e first character from the <em>pending sequence</em>.
+                 */
+                int underflow() override;
+
+            private:
+                sqfs* fs;
+                sqfs_inode* inode;
+                std::vector<char> buffer;
+                sqfs_off_t bytes_already_read = 0;
+            };
+        }
+    }
+}
+
diff --git a/src/libappimage/core/impl/TraversalType1.cpp b/src/libappimage/core/impl/TraversalType1.cpp
new file mode 100644 (file)
index 0000000..ebc9680
--- /dev/null
@@ -0,0 +1,155 @@
+// system
+#include <fcntl.h>
+#include <cstring>
+#include <iostream>
+
+// library
+#include <archive.h>
+#include <archive_entry.h>
+#include <boost/filesystem.hpp>
+
+// local
+#include "appimage/core/AppImage.h"
+#include "appimage/core/exceptions.h"
+#include "appimage/appimage_shared.h"
+#include "TraversalType1.h"
+#include "StreambufType1.h"
+
+using namespace std;
+using namespace appimage::core::impl;
+
+TraversalType1::TraversalType1(const std::string& path) : path(path) {
+    a = archive_read_new();
+    archive_read_support_format_iso9660(a);
+
+    if (archive_read_open_filename(a, path.c_str(), 10240) != ARCHIVE_OK)
+        throw IOError(archive_error_string(a));
+
+    completed = false;
+
+    // Read first entry
+    next();
+}
+
+
+TraversalType1::TraversalType1::~TraversalType1() {
+    archive_read_close(a);
+    archive_read_free(a);
+}
+
+void TraversalType1::next() {
+    if (completed)
+        return;
+
+    readNextHeader();
+    if (!completed) {
+        readEntryData();
+
+        // Skip the "." entry
+        if (entryName == ".")
+            next();
+    }
+}
+
+bool TraversalType1::isCompleted() const { return completed; }
+
+std::string TraversalType1::getEntryPath() const { return entryName; }
+
+appimage::core::PayloadEntryType TraversalType1::getEntryType() const { return entryType; }
+
+string TraversalType1::getEntryLinkTarget() const { return entryLink; }
+
+void TraversalType1::extract(const std::string& target) {
+    // create target parent dir
+    auto parentPath = boost::filesystem::path(target).parent_path();
+    boost::filesystem::create_directories(parentPath);
+
+    // create file with user read and write permissions and only read permission for others and group
+    mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
+    int f = open(target.c_str(), O_WRONLY | O_CREAT | O_TRUNC, mode);
+
+    if (f == -1)
+        throw FileSystemError("Unable to open file: " + target);
+
+    // call the libarchive extract file implementation
+    archive_read_data_into_fd(a, f);
+    close(f);
+}
+
+istream& TraversalType1::read() {
+    // create a new streambuf for reading the current entry
+    auto tmpBuffer = new StreambufType1(a, 1024);
+
+    // replace buffer in the istream
+    entryIStream.rdbuf(tmpBuffer);
+
+    // replace and drop the old buffer
+    entryStreambuf.reset(tmpBuffer);
+
+    return entryIStream;
+}
+
+void TraversalType1::readNextHeader() {
+    int r = archive_read_next_header(a, &entry);
+    if (r == ARCHIVE_EOF) {
+        completed = true;
+        return;
+    }
+
+    if (r != ARCHIVE_OK)
+        throw IOError(archive_error_string(a));
+}
+
+void TraversalType1::readEntryData() {
+    entryName = readEntryName();
+    entryLink = readEntryLink();
+    entryType = readEntryType();
+}
+
+appimage::core::PayloadEntryType TraversalType1::readEntryType() {
+    // Hard links are reported by libarchive as regular files, this a workaround
+    if (!entryLink.empty())
+        return PayloadEntryType::LINK;
+
+    auto entryType = archive_entry_filetype(entry);
+    switch (entryType) {
+        case AE_IFREG:
+            return PayloadEntryType::REGULAR;
+        case AE_IFLNK:
+            return PayloadEntryType::LINK;
+        case AE_IFDIR:
+            return PayloadEntryType::DIR;
+        default:
+            return PayloadEntryType::UNKNOWN;
+    }
+}
+
+std::string TraversalType1::readEntryLink() {
+    auto symlink = archive_entry_symlink(entry);
+    if (symlink)
+        return symlink + 2;
+
+    auto hardlink = archive_entry_hardlink(entry);
+    if (hardlink)
+        return hardlink + 2;
+
+    return std::string();
+}
+
+std::string TraversalType1::readEntryName() {
+    if (completed)
+        return std::string();
+
+    if (entry == nullptr)
+        return std::string();
+
+    const char* entryName = archive_entry_pathname(entry);
+    if (entryName == nullptr)
+        return string();
+
+    // remove ./ prefix from entries names
+    if (strncmp("./", entryName, 2) == 0)
+        return entryName + 2;
+
+    return entryName;
+}
diff --git a/src/libappimage/core/impl/TraversalType1.h b/src/libappimage/core/impl/TraversalType1.h
new file mode 100644 (file)
index 0000000..80a1830
--- /dev/null
@@ -0,0 +1,93 @@
+#pragma once
+
+// system
+#include <memory>
+
+// local
+#include "core/Traversal.h"
+#include "PayloadIStream.h"
+#include "StreambufType1.h"
+
+namespace appimage {
+    namespace core {
+        namespace impl {
+            /**
+             * Provides an implementation of the traversal class for type 1 AppImages. It's based on libarchive.
+             * As libarchive imposes this is a READONLY, ONE WAY, SINGLE PASS traversal implementation.
+             *
+             * See the base class for more details.
+             */
+            class TraversalType1 : public Traversal {
+            public:
+                explicit TraversalType1(const std::string& path);
+
+                // Creating copies of this object is not allowed
+                TraversalType1(TraversalType1& other) = delete;
+
+                // Creating copies of this object is not allowed
+                TraversalType1& operator=(TraversalType1& other) = delete;
+
+                ~TraversalType1();
+
+                void next() override;
+
+                bool isCompleted() const override;
+
+                std::string getEntryPath() const override;
+
+                std::string getEntryLinkTarget() const override;
+
+                PayloadEntryType getEntryType() const override;
+
+                void extract(const std::string& target) override;
+
+                std::istream& read() override;
+
+            private:
+                // control
+                std::string path;
+                bool completed = false;
+
+                // libarchive
+                struct archive* a = {nullptr};
+                struct archive_entry* entry = {nullptr};
+
+                // cache
+                std::string entryName;
+                PayloadEntryType entryType = PayloadEntryType::UNKNOWN;
+                std::string entryLink;
+                PayloadIStream entryIStream;
+                std::unique_ptr<StreambufType1> entryStreambuf;
+
+                /**
+                 * Move to the next header
+                 */
+                void readNextHeader();
+
+                /**
+                 * Read entry data into the cache
+                 */
+                void readEntryData();
+
+                /**
+                 * Read entry name and remove any "." in the prefix
+                 * @return current entry name
+                 */
+                std::string readEntryName();
+
+                /**
+                 * Read and map from archive file types to PayloadEntryType.
+                 * Hard and Symbolic links are classified as "Links"
+                 * @return current entry type
+                 */
+                PayloadEntryType readEntryType();
+
+                /**
+                 * @return entry link if it's a Link type entry otherwise an empty string.
+                 */
+                std::string readEntryLink();
+            };
+        }
+    }
+}
+
diff --git a/src/libappimage/core/impl/TraversalType2.cpp b/src/libappimage/core/impl/TraversalType2.cpp
new file mode 100644 (file)
index 0000000..45521b4
--- /dev/null
@@ -0,0 +1,320 @@
+/*
+ * NOTE ON SQUASHFUSE:
+ * It wasn't designed originally as a library and its headers are somehow broken.
+ * Therefore they must be kept confined.
+ *
+ * keep squashfuse includes on top to avoid _POSIX_C_SOURCE redefinition warning
+*/
+ extern "C" {
+#include <squashfuse.h>
+#include <squashfs_fs.h>
+}
+
+// system
+#include <cstring>
+#include <iostream>
+#include <fstream>
+#include <set>
+
+// libraries
+extern "C" {
+#include <sys/stat.h>
+}
+#include <boost/filesystem.hpp>
+
+
+// local
+#include "appimage/core/AppImage.h"
+#include "appimage/core/exceptions.h"
+#include "PayloadIStream.h"
+#include "StreambufType2.h"
+#include "TraversalType2.h"
+
+using namespace std;
+namespace bf = boost::filesystem;
+using namespace appimage::core::impl;
+
+class TraversalType2::Priv {
+public:
+    explicit Priv(const std::string& path) {
+        // read the offset at which a squashfs image is expected to start
+        ssize_t fs_offset = core::AppImage(path).getPayloadOffset();
+
+        if (fs_offset < 0)
+            throw IOError("get_elf_size error");
+
+        sqfs_err err = sqfs_open_image(&fs, path.c_str(), (size_t) fs_offset);
+
+        if (err != SQFS_OK)
+            throw IOError("sqfs_open_image error: " + path);
+
+        // prepare for traverse
+        rootInodeId = sqfs_inode_root(&fs);
+        err = sqfs_traverse_open(&trv, &fs, rootInodeId);
+        if (err != SQFS_OK) {
+            sqfs_destroy(&fs);
+            throw IOError("sqfs_traverse_open error");
+        }
+    }
+
+    virtual ~Priv() {
+        sqfs_traverse_close(&trv);
+
+        sqfs_destroy(&fs);
+    }
+
+    bool isCompleted() const {
+        return completed;
+    }
+
+    PayloadEntryType getCurrentEntryType() const {
+        return currentEntryType;
+    }
+
+    const string& getCurrentEntryPath() const {
+        return currentEntryPath;
+    }
+
+    const string& getCurrentEntryLink() const {
+        return currentEntryLink;
+    }
+
+    void next() {
+        sqfs_err err;
+        if (!sqfs_traverse_next(&trv, &err))
+            completed = true;
+
+        if (err != SQFS_OK)
+            throw IOError("sqfs_traverse_next error");
+
+        if (!completed) {
+            currentInode = readInode();
+            currentEntryType = readEntryType();
+            currentEntryPath = readEntryName();
+            currentEntryLink = currentEntryType == PayloadEntryType::LINK ? readEntryLink() : std::string();
+        } else {
+            currentEntryType = PayloadEntryType::UNKNOWN;
+            currentEntryPath = std::string();
+            currentEntryLink = std::string();
+        }
+    }
+
+    void extract(const std::string& target) {
+        sqfs_inode inode;
+        if (sqfs_inode_get(&fs, &inode, trv.entry.inode))
+            throw IOError("sqfs_inode_get error");
+
+        // create target parent dir
+        auto parentPath = bf::path(target).parent_path();
+        bf::create_directories(parentPath);
+
+        // handle each inode type properly
+        switch (inode.base.inode_type) {
+            case SQUASHFS_DIR_TYPE:
+            case SQUASHFS_LDIR_TYPE:
+                extractDir(target);
+                break;
+            case SQUASHFS_REG_TYPE:
+            case SQUASHFS_LREG_TYPE:
+                extractFile(inode, target);
+                break;
+            case SQUASHFS_SYMLINK_TYPE:
+            case SQUASHFS_LSYMLINK_TYPE:
+                extractSymlink(inode, target);
+                break;
+            default:
+                throw AppImageError("AppImage Type 2 inode.base.inode_type " +
+                                    std::to_string(inode.base.inode_type) + " not supported yet");
+        }
+    }
+
+    istream& read() {
+        // create a streambuf for reading the inode contents
+        auto tmpBuffer = new StreambufType2(&fs, &currentInode, 1024);
+
+        // replace buffer of the istream
+        entryIStream.rdbuf(tmpBuffer);
+
+        // replace and drop old buffer
+        entryStreamBuf.reset(tmpBuffer);
+
+        return entryIStream;
+    }
+
+private:
+    std::string path;
+    bool completed = false;
+
+    // squash fuse context
+    struct sqfs fs = {};
+    sqfs_traverse trv = {};
+    sqfs_inode_id rootInodeId = 0;
+    sqfs_inode currentInode = {};
+
+    // Current entry data cache
+    PayloadEntryType currentEntryType = PayloadEntryType::UNKNOWN;
+    std::string currentEntryPath;
+    std::string currentEntryLink;
+
+    PayloadIStream entryIStream;
+    std::unique_ptr<StreambufType2> entryStreamBuf;
+
+    sqfs_inode readInode() {
+        sqfs_inode inode;
+        if (sqfs_inode_get(&fs, &inode, trv.entry.inode))
+            throw IOError("sqfs_inode_get error");
+
+        return inode;
+    }
+
+    /**
+    * Read the current entry type from the underlying implementation.
+    * @return Current entry type
+    */
+    appimage::core::PayloadEntryType readEntryType() const {
+        // squashfs traversal follows a DFS pattern but directories are "visited" twice, when they are reached and when they
+        // are left. This check is to properly identify the second scenario.
+        if (trv.dir_end)
+            return PayloadEntryType::DIR;
+
+        switch (trv.entry.type) {
+            case SQUASHFS_REG_TYPE:
+            case SQUASHFS_LREG_TYPE:
+                return PayloadEntryType::REGULAR;
+
+            case SQUASHFS_SYMLINK_TYPE:
+            case SQUASHFS_LSYMLINK_TYPE:
+                return PayloadEntryType::LINK;
+
+            case SQUASHFS_DIR_TYPE:
+            case SQUASHFS_LDIR_TYPE:
+                return PayloadEntryType::DIR;
+
+            default:
+                return PayloadEntryType::UNKNOWN;
+        }
+    }
+
+    /**
+    * Read the current entry path from the underlying implementation.
+    * @return Current entry path
+    */
+    std::string readEntryName() const {
+        if (trv.path != nullptr)
+            return trv.path;
+        else
+            return string();
+    }
+
+    /**
+     * Read the current entry link path from the underlying implementation.
+     * @return Current link entry path or an empty string if the entry is not a link.
+     * */
+    std::string readEntryLink() {
+        // read the target link path size
+        size_t size;
+        auto err = sqfs_readlink(&fs, &currentInode, nullptr, &size);
+        if (err != SQFS_OK)
+            throw IOError("sqfs_readlink error");
+
+        char buf[size];
+
+        // read the target link in buf
+        err = sqfs_readlink(&fs, &currentInode, buf, &size);
+        if (err != SQFS_OK)
+            throw IOError("sqfs_readlink error");
+
+        // If the returned string is not NULL terminated a buffer overflow may occur, creating the string this way
+        // prevents it
+        return std::string(buf, buf + size - 1);
+    }
+
+    /**
+    * Creates a directory at target.
+    * @param target
+    */
+    void extractDir(const std::string& target) {
+        // The directory doesn't exists
+        if (access(target.c_str(), F_OK) == -1) {
+            // Create new directory with 755 permissions
+            if (mkdir(target.c_str(), 0755) == -1)
+                throw FileSystemError("mkdir error at " + target);
+        }
+    }
+
+
+    /**
+    * extract the file pointed by <inode> contents at <target>
+    * @param inode file
+    * @param target path
+    */
+    void extractFile(sqfs_inode inode, const std::string& target) {
+        // open read stream
+        auto& istream = read();
+
+        // open write stream
+        ofstream targetFile(target);
+
+        // transfer data
+        targetFile << istream.rdbuf();
+        targetFile.close();
+
+        // set file stats
+        chmod(target.c_str(), inode.base.mode);
+    }
+
+
+    /**
+    * extract the symlink pointed by <inode> at <target>
+    * @param inode symlink
+    * @param target path
+    */
+    void extractSymlink(sqfs_inode inode, const std::string& target) {
+        // read the target link in buf
+        int ret = unlink(currentEntryLink.c_str());
+        if (ret != 0 && errno != ENOENT)
+            throw IOError("unlink error at " + target);
+
+        ret = symlink(currentEntryLink.c_str(), target.c_str());
+        if (ret != 0)
+            throw IOError("symlink error at " + target);
+    }
+};
+
+TraversalType2::TraversalType2(std::string path) : d(new Priv(path)) {
+    // The traversal starts pointing to an empty entry, fetch first entry to be in a valid stated
+    next();
+}
+
+TraversalType2::~TraversalType2() = default;
+
+void TraversalType2::next() {
+    d->next();
+}
+
+
+bool TraversalType2::isCompleted() const {
+    return d->isCompleted();
+}
+
+std::string TraversalType2::getEntryPath() const {
+    return d->getCurrentEntryPath();
+}
+
+appimage::core::PayloadEntryType TraversalType2::getEntryType() const {
+    return d->getCurrentEntryType();
+}
+
+void TraversalType2::extract(const std::string& target) {
+    d->extract(target);
+}
+
+
+istream& TraversalType2::read() {
+    return d->read();
+
+}
+
+string TraversalType2::getEntryLinkTarget() const {
+    return d->getCurrentEntryLink();
+}
diff --git a/src/libappimage/core/impl/TraversalType2.h b/src/libappimage/core/impl/TraversalType2.h
new file mode 100644 (file)
index 0000000..7f2960c
--- /dev/null
@@ -0,0 +1,53 @@
+#pragma once
+
+// system
+#include <memory>
+
+// local
+#include "core/Traversal.h"
+#include "PayloadIStream.h"
+
+namespace appimage {
+    namespace core {
+        namespace impl {
+            /**
+             * Provides an implementation of the traversal class for type 2 AppImages. It's based on squashfuse.
+             * The current implementation has the following limitations: READONLY, ONE WAY, SINGLE PASS.
+             *
+             * See the base class for more details.
+             */
+            class TraversalType2 : public Traversal {
+            public:
+                explicit TraversalType2(std::string path);
+
+                // Creating copies of this object is not allowed
+                TraversalType2(TraversalType2& other) = delete;
+
+                // Creating copies of this object is not allowed
+                TraversalType2& operator=(TraversalType2& other) = delete;
+
+                ~TraversalType2();
+
+                void next() override;
+
+                bool isCompleted() const override;
+
+                std::string getEntryPath() const override;
+
+                std::string getEntryLinkTarget() const override;
+
+                PayloadEntryType getEntryType() const override;
+
+                void extract(const std::string& target) override;
+
+                std::istream& read() override;
+
+            private:
+                // Keep squashfuse private, it's too unstable to go into the wild
+                class Priv;
+
+                std::unique_ptr<Priv> d;
+            };
+        }
+    }
+}
diff --git a/src/libappimage/desktop_integration/CMakeLists.txt b/src/libappimage/desktop_integration/CMakeLists.txt
new file mode 100644 (file)
index 0000000..10d4df6
--- /dev/null
@@ -0,0 +1,39 @@
+set(
+    appimage_desktop_integration_sources
+    IntegrationManager.cpp
+    integrator/Integrator.cpp
+    integrator/DesktopEntryEditError.h
+    integrator/DesktopEntryEditor.cpp
+)
+
+if(LIBAPPIMAGE_THUMBNAILER_ENABLED)
+    list(APPEND appimage_desktop_integration_sources "Thumbnailer.cpp")
+endif()
+
+add_library(appimage_desktop_integration OBJECT ${appimage_desktop_integration_sources})
+
+# Include interface directories from the libs used. Ideally we will use target_link_libraries
+# but it's not supported in cmake 3.8.
+# https://cmake.org/cmake/help/latest/command/target_link_libraries.html#linking-object-libraries
+target_include_directories(
+    appimage_desktop_integration
+    PUBLIC $<TARGET_PROPERTY:libappimage_hashlib,INTERFACE_INCLUDE_DIRECTORIES>
+    PRIVATE $<TARGET_PROPERTY:libarchive,INTERFACE_INCLUDE_DIRECTORIES>
+    PRIVATE $<TARGET_PROPERTY:libsquashfuse,INTERFACE_INCLUDE_DIRECTORIES>
+    PRIVATE $<TARGET_PROPERTY:libglib,INTERFACE_INCLUDE_DIRECTORIES>
+    PRIVATE $<TARGET_PROPERTY:Boost::filesystem,INTERFACE_INCLUDE_DIRECTORIES>
+    PRIVATE $<TARGET_PROPERTY:XdgUtils::DesktopEntry,INTERFACE_INCLUDE_DIRECTORIES>
+    PRIVATE $<TARGET_PROPERTY:XdgUtils::BaseDir,INTERFACE_INCLUDE_DIRECTORIES>
+    PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
+    PRIVATE $<BUILD_INTERFACE:${PROJECT_BINARY_DIR}/generated-headers>
+)
+
+configure_libappimage_module(appimage_desktop_integration)
+add_dependencies(appimage_desktop_integration
+    libappimage_hashlib
+    libarchive
+    libsquashfuse
+    Boost::filesystem
+    XdgUtils::DesktopEntry
+    XdgUtils::BaseDir
+)
diff --git a/src/libappimage/desktop_integration/IntegrationManager.cpp b/src/libappimage/desktop_integration/IntegrationManager.cpp
new file mode 100644 (file)
index 0000000..bd098a6
--- /dev/null
@@ -0,0 +1,154 @@
+// system
+#include <sstream>
+
+// libraries
+#include <boost/filesystem.hpp>
+#include <boost/algorithm/string.hpp>
+#include <XdgUtils/BaseDir/BaseDir.h>
+#include <XdgUtils/DesktopEntry/DesktopEntry.h>
+
+// local
+#include <appimage/desktop_integration/IntegrationManager.h>
+#include <appimage/desktop_integration/exceptions.h>
+#include <appimage/utils/ResourcesExtractor.h>
+#include "integrator/Integrator.h"
+#include "utils/hashlib.h"
+#include "utils/path_utils.h"
+#include "constants.h"
+
+#ifdef LIBAPPIMAGE_THUMBNAILER_ENABLED
+
+#include "Thumbnailer.h"
+#endif
+
+namespace bf = boost::filesystem;
+
+namespace appimage {
+    namespace desktop_integration {
+        class IntegrationManager::Private {
+        public:
+            bf::path xdgDataHome;
+
+#ifdef LIBAPPIMAGE_THUMBNAILER_ENABLED
+            Thumbnailer thumbnailer;
+#endif
+
+            std::string generateAppImageId(const std::string& appImagePath) {
+                // Generate AppImage Id
+                std::string md5 = utils::hashPath(appImagePath);
+
+                return VENDOR_PREFIX + "_" + md5;
+            }
+
+            /**
+             * Explore <dir> recursively and remove files that contain <hint> in their name.
+             * @param dir
+             * @param hint
+             */
+            void removeMatchingFiles(const bf::path& dir, const std::string& hint) {
+                try {
+                    for (bf::recursive_directory_iterator it(dir), eit; it != eit; ++it) {
+                        if (!bf::is_directory(it->path()) && it->path().string().find(hint) != std::string::npos)
+                            bf::remove(it->path());
+                    }
+                } catch (const bf::filesystem_error&) {}
+            }
+        };
+
+        IntegrationManager::IntegrationManager() : d(new Private) {
+            d->xdgDataHome = XdgUtils::BaseDir::XdgDataHome();
+        }
+
+        IntegrationManager::IntegrationManager(const std::string& xdgDataHome) : d(new Private) {
+            if (xdgDataHome.empty() || !bf::is_directory(xdgDataHome))
+                throw DesktopIntegrationError("Invalid XDG_DATA_HOME: " + xdgDataHome);
+
+            d->xdgDataHome = xdgDataHome;
+        }
+
+        void IntegrationManager::registerAppImage(const core::AppImage& appImage) const {
+            try {
+                integrator::Integrator i(appImage, d->xdgDataHome);
+                i.integrate();
+            } catch (...) {
+                // Remove any file created during the integration process
+                unregisterAppImage(appImage.getPath());
+
+                // Rethrow
+                throw;
+            }
+        }
+
+        bool IntegrationManager::isARegisteredAppImage(const std::string& appImagePath) const {
+            // Generate AppImage Id
+            const auto& appImageId = d->generateAppImageId(appImagePath);
+
+            // look for a desktop entry file with the AppImage Id in its name
+            bf::path appsPath = d->xdgDataHome / "applications";
+
+            try {
+                for (bf::recursive_directory_iterator it(appsPath), eit; it != eit; ++it) {
+                    if (!bf::is_directory(it->path()) && it->path().string().find(appImageId) != std::string::npos)
+                        return true;
+                }
+            } catch (const bf::filesystem_error&) {}
+
+            return false;
+        }
+
+        bool IntegrationManager::shallAppImageBeRegistered(const core::AppImage& appImage) const {
+            try {
+                utils::ResourcesExtractor extractor(appImage);
+                auto desktopEntryPath = extractor.getDesktopEntryPath();
+                const auto desktopEntryData = extractor.extractText(desktopEntryPath);
+
+                XdgUtils::DesktopEntry::DesktopEntry entry(desktopEntryData);
+
+                auto integrateValue = entry.get("Desktop Entry/X-AppImage-Integrate");
+                boost::algorithm::erase_all(integrateValue, " ");
+                boost::algorithm::to_lower(integrateValue);
+
+                if (integrateValue == "false")
+                    return false;
+
+                auto terminalValue = entry.get("Desktop Entry/Terminal");
+                boost::algorithm::erase_all(terminalValue, " ");
+                boost::algorithm::to_lower(terminalValue);
+                if (terminalValue == "true")
+                    return false;
+
+            } catch (const appimage::core::AppImageError& error) {
+                throw DesktopIntegrationError("Unable to read the AppImage");
+            }
+
+
+            return true;
+        }
+
+        void IntegrationManager::unregisterAppImage(const std::string& appImagePath) const {
+            // Generate AppImage Id
+            const auto appImageId = d->generateAppImageId(appImagePath);
+
+            // remove files with the
+            d->removeMatchingFiles(d->xdgDataHome / "applications", appImageId);
+            d->removeMatchingFiles(d->xdgDataHome / "icons", appImageId);
+            d->removeMatchingFiles(d->xdgDataHome / "mime/packages", appImageId);
+        }
+
+#ifdef LIBAPPIMAGE_THUMBNAILER_ENABLED
+        void IntegrationManager::generateThumbnails(const core::AppImage& appImage) const {
+            d->thumbnailer.create(appImage);
+        }
+
+        void IntegrationManager::removeThumbnails(const std::string& appImagePath) const {
+            d->thumbnailer.remove(appImagePath);
+        }
+#endif
+
+        IntegrationManager::IntegrationManager(const IntegrationManager& other) = default;
+
+        IntegrationManager& IntegrationManager::operator=(const IntegrationManager& other) = default;
+
+        IntegrationManager::~IntegrationManager() = default;
+    }
+}
diff --git a/src/libappimage/desktop_integration/Thumbnailer.cpp b/src/libappimage/desktop_integration/Thumbnailer.cpp
new file mode 100644 (file)
index 0000000..bbce150
--- /dev/null
@@ -0,0 +1,159 @@
+// system
+#include <sstream>
+#include <fstream>
+
+// libraries
+#include <boost/filesystem.hpp>
+#include <boost/algorithm/string.hpp>
+#include <XdgUtils/DesktopEntry/DesktopEntry.h>
+#include <XdgUtils/BaseDir/BaseDir.h>
+
+
+// local
+#include "utils/Logger.h"
+#include "utils/IconHandle.h"
+#include "utils/path_utils.h"
+#include "Thumbnailer.h"
+
+using namespace appimage::utils;
+namespace bf = boost::filesystem;
+
+namespace appimage {
+    namespace desktop_integration {
+        Thumbnailer::Thumbnailer() : xdgCacheHome(XdgUtils::BaseDir::Home() + "/.cache") {}
+
+        Thumbnailer::Thumbnailer(const std::string& xdgCacheHome) : xdgCacheHome(xdgCacheHome) {
+            /* XDG_CACHE_HOME path is required to deploy the thumbnails */
+            if (Thumbnailer::xdgCacheHome.empty())
+                Thumbnailer::xdgCacheHome = XdgUtils::BaseDir::Home() + "/.cache";
+        }
+
+        void Thumbnailer::create(const core::AppImage& appImage) {
+            utils::ResourcesExtractor extractor(appImage);
+
+            /* Just the application main icon will be used to generate the thumbnails */
+            std::string appIcon = getAppIconName(extractor);
+
+            /* According to the xdg thumbnails spec files should be named after the
+             * md5 sum of it's canonical path. */
+            std::string canonicalPathMd5 = hashPath(appImage.getPath());
+
+            auto appIconsPaths = extractor.getIconFilePaths(appIcon);
+
+            // Look for the bests icons to to be used while generating the thumbnails
+            auto normalIconPath = getIconPath(appIconsPaths, "128x128");
+            auto largeIconPath = getIconPath(appIconsPaths, "256x256");
+
+            auto iconsData = extractor.extract(std::vector<std::string>{normalIconPath, largeIconPath});
+
+            generateNormalSizeThumbnail(canonicalPathMd5, iconsData[normalIconPath]);
+            generateLargeSizeThumbnail(canonicalPathMd5, iconsData[largeIconPath]);
+        }
+
+        void Thumbnailer::remove(const std::string& appImagePath) {
+            /* Every resource file related with this appimage has the md5 sum of the appimage canonical
+             * path in its name, we are going to use this to recreate the file names */
+            std::string canonicalPathMd5 = hashPath(appImagePath);
+            bf::path normalThumbnailPath = getNormalThumbnailPath(canonicalPathMd5);
+            bf::path largeThumbnailPath = getLargeThumbnailPath(canonicalPathMd5);
+
+            bf::remove(normalThumbnailPath);
+            bf::remove(largeThumbnailPath);
+        }
+
+        void Thumbnailer::generateNormalSizeThumbnail(const std::string& canonicalPathMd5,
+                                                      std::vector<char>& normalIconData) const {
+
+            bf::path normalThumbnailPath = getNormalThumbnailPath(canonicalPathMd5);
+
+            /* It required that the folders were the thumbnails will be deployed to exists */
+            bf::create_directories(normalThumbnailPath.parent_path());
+
+            try {
+                IconHandle iconHandle(normalIconData);
+                /* large thumbnails are 128x128, let's be sure of it*/
+                iconHandle.setSize(128);
+                iconHandle.save(normalThumbnailPath.string(), "png");
+                return;
+            } catch (const IconHandleError& error) {
+                /* we fail to resize the icon because it's in an unknown format or some other reason
+                 * we just have left to write it down unchanged and hope for the best. */
+                Logger::warning(std::string("Unable to resize the application icon into a 128x128 image: \"") +
+                                error.what() + "\". It will be written unchanged.");
+            }
+
+            // It wasn't possible to generate a thumbnail, therefore the the icon will be written unchanged
+            std::ofstream out(normalThumbnailPath.string());
+            out.write(normalIconData.data(), normalIconData.size());
+        }
+
+        void Thumbnailer::generateLargeSizeThumbnail(const std::string& canonicalPathMd5,
+                                                     std::vector<char>& largeIconData) const {
+            bf::path largeThumbnailPath = getLargeThumbnailPath(canonicalPathMd5);
+
+            /* It required that the folders were the thumbnails will be deployed to exists */
+            bf::create_directories(largeThumbnailPath.parent_path());
+
+            try {
+                IconHandle iconHandle(largeIconData);
+                /* large thumbnails are 256x256, let's be sure of it*/
+                iconHandle.setSize(256);
+
+                /* thumbnails are always png */
+                iconHandle.save(largeThumbnailPath.string(), "png");
+                return;
+            } catch (const IconHandleError& error) {
+                /* we fail to resize the icon because it's in an unknown format or some other reason
+                 * we just have left to write it down unchanged and hope for the best. */
+                Logger::warning(std::string("Unable to resize the application icon into a 256x256 image: \"") +
+                                error.what() + "\". It will be written unchanged.");
+            }
+
+            // It wasn't possible to generate a thumbnail, therefore the the icon will be written unchanged
+            std::ofstream out(largeThumbnailPath.string());
+            out.write(largeIconData.data(), largeIconData.size());
+            out.close();
+        }
+
+
+        bf::path Thumbnailer::getNormalThumbnailPath(const std::string& canonicalPathMd5) const {
+            bf::path xdgCacheHomePath(xdgCacheHome);
+
+            bf::path normalThumbnailPath = xdgCacheHomePath / normalThumbnailsPrefix /
+                                           (canonicalPathMd5 + thumbnailFileExtension);
+            return normalThumbnailPath;
+        }
+
+        bf::path Thumbnailer::getLargeThumbnailPath(const std::string& canonicalPathMd5) const {
+            bf::path xdgCacheHomePath(xdgCacheHome);
+
+            bf::path largeThumbnailPath = xdgCacheHomePath / largeThumbnailPrefix /
+                                          (canonicalPathMd5 + thumbnailFileExtension);
+            return largeThumbnailPath;
+        }
+
+        std::string Thumbnailer::getIconPath(std::vector<std::string> appIcons, const std::string& size) {
+            /* look for an icon with the size required or an scalable one. It will be resized latter */
+            for (const auto& itr: appIcons) {
+                if (itr.find(size) != std::string::npos ||
+                    itr.find("/scalable/") != std::string::npos) {
+                    return itr;
+                }
+            }
+
+            // Fallback to ".DirIcon"
+            return ".DirIcon";
+
+        }
+
+        std::string Thumbnailer::getAppIconName(const ResourcesExtractor& resourcesExtractor) const {
+            auto desktopEntryPath = resourcesExtractor.getDesktopEntryPath();
+            auto desktopEntryData = resourcesExtractor.extractText(desktopEntryPath);
+
+            XdgUtils::DesktopEntry::DesktopEntry entry(desktopEntryData);
+            return entry.get("Desktop Entry/Icon");
+        }
+
+        Thumbnailer::~Thumbnailer() = default;
+    }
+}
diff --git a/src/libappimage/desktop_integration/Thumbnailer.h b/src/libappimage/desktop_integration/Thumbnailer.h
new file mode 100644 (file)
index 0000000..06313ed
--- /dev/null
@@ -0,0 +1,85 @@
+// system
+#include <string>
+
+// libraries
+#include <boost/filesystem.hpp>
+
+// local
+#include <appimage/core/AppImage.h>
+#include <appimage/utils/ResourcesExtractor.h>
+
+namespace bf = boost::filesystem;
+
+namespace appimage {
+    namespace desktop_integration {
+        /**
+         * Thumbnails generator for AppImage files
+         *
+         * Follows the Thumbnail Managing Standard by FreeDesktop
+         * https://specifications.freedesktop.org/thumbnail-spec/0.8.0/index.html
+         */
+        class Thumbnailer {
+        public:
+            /**
+             * Creates a Thumbnailer that will create and remove thumbnails at the user XDG_CACHE_HOME dir.
+             */
+            explicit Thumbnailer();
+
+            /**
+             * Creates a Thumbnailer that will create and remove thumbnails at the dir pointed by <xdgCacheHome> .
+             */
+            explicit Thumbnailer(const std::string& xdgCacheHome);
+
+            /**
+             * @brief Generate thumbnails for the given <appImagePath>
+             *
+             * Thumbnail generation is performed according to the Freedesktop specification.
+             * Two images of 128x128 and 256x256 will be placed at "$XDG_CACHE_HOME/thumbnails/normal" and
+             * "$XDG_CACHE_HOME/thumbnails/large/" respectively. The thumbnails name will be formed by a md5 sum of
+             * the absolute canonical URI for the original file whit ".png" as extension.
+             *
+             * Full FreeDesktop Thumbnails spec: https://specifications.freedesktop.org/thumbnail-spec/0.8.0/x227.html
+             *
+             * @param appImage
+             */
+            void create(const core::AppImage& appImage);
+
+            /**
+             * @brief remove <appImage> thumbnails
+             *
+             * Will find and remove every thumbnail related to the file pointed by the AppImage path. The files will
+             * be identified following the rules described in the Full FreeDesktop Thumbnails spec. Which is available
+             * at: https://specifications.freedesktop.org/thumbnail-spec/0.8.0/x227.html
+             * @param appImagePath
+             */
+            void remove(const std::string& appImagePath);
+
+            virtual ~Thumbnailer();
+
+        private:
+            bf::path xdgCacheHome;
+
+            static constexpr const char* thumbnailFileExtension = ".png";
+
+            static constexpr const char* normalThumbnailsPrefix = "thumbnails/normal";
+
+            bf::path getNormalThumbnailPath(const std::string& canonicalPathMd5) const;
+
+            static constexpr const char* largeThumbnailPrefix = "thumbnails/large";
+
+            bf::path getLargeThumbnailPath(const std::string& canonicalPathMd5) const;
+
+            std::string getAppIconName(const utils::ResourcesExtractor& resourcesExtractor) const;
+
+            std::string getIconPath(std::vector<std::string> appIcons, const std::string& size);
+
+            void generateNormalSizeThumbnail(const std::string& canonicalPathMd5,
+                                             std::vector<char>& normalIconData) const;
+
+            void generateLargeSizeThumbnail(const std::string& canonicalPathMd5,
+                                            std::vector<char>& largeIconData) const;
+        };
+    }
+}
+
+
diff --git a/src/libappimage/desktop_integration/constants.h b/src/libappimage/desktop_integration/constants.h
new file mode 100644 (file)
index 0000000..e328336
--- /dev/null
@@ -0,0 +1,9 @@
+#pragma once
+
+#include <string>
+
+namespace appimage{
+    namespace desktop_integration {
+        static const std::string VENDOR_PREFIX = "appimagekit";
+    }
+}
diff --git a/src/libappimage/desktop_integration/integrator/DesktopEntryEditError.h b/src/libappimage/desktop_integration/integrator/DesktopEntryEditError.h
new file mode 100644 (file)
index 0000000..f7cc70d
--- /dev/null
@@ -0,0 +1,21 @@
+#pragma once
+
+// system
+#include <stdexcept>
+
+// local
+#include <appimage/desktop_integration/exceptions.h>
+
+namespace appimage {
+    namespace desktop_integration {
+        namespace integrator {
+            /**
+             * Throw when something goes wrong with the desktop entry edition process.
+             */
+            class DesktopEntryEditError : public DesktopIntegrationError {
+            public:
+                explicit DesktopEntryEditError(const std::string& what) : DesktopIntegrationError(what) {}
+            };
+        }
+    }
+}
diff --git a/src/libappimage/desktop_integration/integrator/DesktopEntryEditor.cpp b/src/libappimage/desktop_integration/integrator/DesktopEntryEditor.cpp
new file mode 100644 (file)
index 0000000..ebe9650
--- /dev/null
@@ -0,0 +1,143 @@
+// system
+#include <string>
+#include <sstream>
+#include <algorithm>
+
+// libraries
+#include <XdgUtils/DesktopEntry/DesktopEntryExecValue.h>
+#include <XdgUtils/DesktopEntry/DesktopEntryStringsValue.h>
+#include <XdgUtils/DesktopEntry/DesktopEntryKeyPath.h>
+#include <utils/StringSanitizer.h>
+
+// local
+#include "DesktopEntryEditor.h"
+#include "DesktopEntryEditError.h"
+
+
+using namespace XdgUtils::DesktopEntry;
+
+namespace appimage {
+    namespace desktop_integration {
+        namespace integrator {
+            void DesktopEntryEditor::setAppImagePath(const std::string& appImagePath) {
+                DesktopEntryEditor::appImagePath = appImagePath;
+            }
+
+            void DesktopEntryEditor::edit(XdgUtils::DesktopEntry::DesktopEntry& desktopEntry) {
+                if (!desktopEntry.exists("Desktop Entry/Exec"))
+                    throw DesktopEntryEditError("Missing Desktop Entry");
+
+                // set default vendor prefix
+                if (vendorPrefix.empty())
+                    vendorPrefix = "appimagekit";
+
+                setExecPaths(desktopEntry);
+
+                setIcons(desktopEntry);
+
+                appendVersionToName(desktopEntry);
+
+                // set identifier
+                desktopEntry.set("Desktop Entry/X-AppImage-Identifier", identifier);
+            }
+
+            void DesktopEntryEditor::setAppImageVersion(const std::string& appImageVersion) {
+                DesktopEntryEditor::appImageVersion = appImageVersion;
+            }
+
+            void DesktopEntryEditor::setIdentifier(const std::string& uuid) {
+                DesktopEntryEditor::identifier = uuid;
+            }
+
+            void DesktopEntryEditor::setVendorPrefix(const std::string& vendorPrefix) {
+                DesktopEntryEditor::vendorPrefix = vendorPrefix;
+            }
+
+            void DesktopEntryEditor::appendVersionToName(XdgUtils::DesktopEntry::DesktopEntry& desktopEntry) {
+                // AppImage Version can be set from an external source like appstream.xml
+                if (!appImageVersion.empty())
+                    desktopEntry.set("Desktop Entry/X-AppImage-Version", appImageVersion);
+
+                if (desktopEntry.exists("Desktop Entry/X-AppImage-Version")) {
+                    // The AppImage Version can also be set by the author in the Desktop Entry
+                    appImageVersion = desktopEntry.get("Desktop Entry/X-AppImage-Version");
+
+                    // find name entries
+                    std::vector<std::string> nameEntriesPaths;
+                    for (const auto& path: desktopEntry.paths())
+                        if (path.find("Desktop Entry/Name") != std::string::npos)
+                            nameEntriesPaths.emplace_back(path);
+
+                    for (const auto& path: nameEntriesPaths) {
+                        std::string name = desktopEntry.get(path);
+
+                        // Skip version if it's already part of the name
+                        if (name.find(appImageVersion) != std::string::npos)
+                            continue;
+
+                        // create new name as "<oldApplicationName> (<appImageVersion>)"
+                        std::stringstream newName;
+                        newName << name << " (" << appImageVersion << ')';
+                        desktopEntry.set(path, newName.str());
+
+                        // Save old name value at <group>/X-AppImage-Old-Name<locale>
+                        DesktopEntryKeyPath oldValueKeyPath(path);
+                        oldValueKeyPath.setKey("X-AppImage-Old-Name");
+                        desktopEntry.set(oldValueKeyPath.string(), name);
+                    }
+                }
+            }
+
+            void DesktopEntryEditor::setIcons(XdgUtils::DesktopEntry::DesktopEntry& desktopEntry) {
+                if (identifier.empty())
+                    throw DesktopEntryEditError("Missing AppImage UUID");
+
+                // retrieve all icon key paths
+                std::vector<std::string> iconEntriesPaths;
+                for (const auto& path: desktopEntry.paths())
+                    if (path.find("/Icon") != std::string::npos)
+                        iconEntriesPaths.emplace_back(path);
+
+                // add icon names
+                for (const auto& path: iconEntriesPaths) {
+                    std::string iconName = desktopEntry.get(path);
+
+                    // create new icon name as "<vendorPrefix>_<uuid>_<oldIconName>"
+                    std::stringstream newIcon;
+
+                    // we don't trust the icon name inside the desktop file, so we sanitize the filename before
+                    // calculating the integrated icon's path
+                    // this keeps the filename understandable while mitigating risks for potential attacks
+                    newIcon << vendorPrefix << "_" << identifier << "_" << StringSanitizer(iconName).sanitizeForPath();
+
+                    desktopEntry.set(path, newIcon.str());
+
+                    // Save old icon value at <group>/X-AppImage-Old-Icon<locale>
+                    DesktopEntryKeyPath oldValueKeyPath(path);
+                    oldValueKeyPath.setKey("X-AppImage-Old-Icon");
+                    desktopEntry.set(oldValueKeyPath.string(), iconName);
+                }
+            }
+
+            void DesktopEntryEditor::setExecPaths(XdgUtils::DesktopEntry::DesktopEntry& desktopEntry) {
+                // Edit "Desktop Entry/Exec"
+                DesktopEntryExecValue execValue(desktopEntry.get("Desktop Entry/Exec"));
+                execValue[0] = appImagePath;
+                desktopEntry.set("Desktop Entry/Exec", execValue.dump());
+
+                // Edit TryExec
+                desktopEntry.set("Desktop Entry/TryExec", appImagePath);
+
+                // modify actions Exec entry
+                DesktopEntryStringsValue actions(desktopEntry.get("Desktop Entry/Actions"));
+                for (int i = 0; i < actions.size(); i++) {
+                    std::string keyPath = "Desktop Action " + actions[i] + "/Exec";
+
+                    DesktopEntryExecValue actionExecValue(desktopEntry.get(keyPath));
+                    actionExecValue[0] = appImagePath;
+                    desktopEntry.set(keyPath, actionExecValue.dump());
+                }
+            }
+        }
+    }
+}
diff --git a/src/libappimage/desktop_integration/integrator/DesktopEntryEditor.h b/src/libappimage/desktop_integration/integrator/DesktopEntryEditor.h
new file mode 100644 (file)
index 0000000..33a499f
--- /dev/null
@@ -0,0 +1,78 @@
+#pragma once
+
+// system
+#include <set>
+#include <string>
+
+// local
+#include <XdgUtils/DesktopEntry/DesktopEntry.h>
+
+namespace appimage {
+    namespace desktop_integration {
+        namespace integrator {
+            /**
+             * @brief Edit a Desktop Entry from an AppImage to deploy it into the system.
+             *
+             * Taking a <desktopEntry> as input this class allows to reset the 'Exec', and 'Icon' entries to new values.
+             */
+            class DesktopEntryEditor {
+            public:
+                /**
+                 * @param appImagePath
+                 */
+                void setAppImagePath(const std::string& appImagePath);
+
+                /**
+                 * @param appImageVersion
+                 */
+                void setAppImageVersion(const std::string& appImageVersion);
+
+                /**
+                 * @param vendorPrefix usually the AppImagePath md5 sum
+                 */
+                void setVendorPrefix(const std::string& vendorPrefix);
+
+                /**
+                 * Set the uuid that will identify the deployed AppImage resources.
+                 * Usually this value is a md5 sum of the AppImage path.
+                 * @param uuid
+                 */
+                void setIdentifier(const std::string& uuid);
+
+                /**
+                 * Modifies the Desktop Entry according to the set parameters.
+                 * @param desktopEntry
+                 */
+                void edit(XdgUtils::DesktopEntry::DesktopEntry& desktopEntry);
+
+            private:
+                std::string identifier;
+                std::string vendorPrefix;
+                std::string appImagePath;
+                std::string appImageVersion;
+
+                /**
+                 * Set Exec and TryExec entries in the 'Desktop Entry' and 'Desktop Action' groups pointing to the
+                 * <appImagePath>.
+                 */
+                void setExecPaths(XdgUtils::DesktopEntry::DesktopEntry& entry);
+
+                /**
+                 * Set Icon entries in the 'Desktop Entry' and 'Desktop Action' groups pointing to the new icon names.
+                 * The new icon names have the following structure <vendorPrefix>_<uuid>_<oldIconName>
+                 */
+                void setIcons(XdgUtils::DesktopEntry::DesktopEntry& entry);
+
+                /**
+                 * Append the <appImageVersion> to the Name entries in the 'Desktop Entry' group.
+                 * The new names will have the following structure "<oldApplicationName> (<appImageVersion>)"
+                 *
+                 * If the appImageVersion is not set the value from "Desktop Entry/X-AppImage-Version" will be used instead.
+                 * If none of both options are valid the names will remain unchanged.
+                 */
+                void appendVersionToName(XdgUtils::DesktopEntry::DesktopEntry& entry);
+            };
+
+        }
+    }
+}
diff --git a/src/libappimage/desktop_integration/integrator/Integrator.cpp b/src/libappimage/desktop_integration/integrator/Integrator.cpp
new file mode 100644 (file)
index 0000000..2341121
--- /dev/null
@@ -0,0 +1,331 @@
+// system
+#include <cstdlib>
+#include <sstream>
+#include <utility>
+#include <vector>
+#include <iostream>
+#include <fstream>
+
+// libraries
+#include <boost/filesystem.hpp>
+#include <boost/algorithm/string/trim.hpp>
+#include <boost/algorithm/string/replace.hpp>
+#include <boost/algorithm/string.hpp>
+#include <XdgUtils/DesktopEntry/DesktopEntry.h>
+#include <XdgUtils/BaseDir/BaseDir.h>
+
+// local
+#include <appimage/core/AppImage.h>
+#include <appimage/desktop_integration/exceptions.h>
+#include <appimage/utils/ResourcesExtractor.h>
+#include <constants.h>
+#include "utils/Logger.h"
+#include "utils/hashlib.h"
+#include "utils/IconHandle.h"
+#include "utils/path_utils.h"
+#include "utils/StringSanitizer.h"
+#include "DesktopEntryEditor.h"
+#include "Integrator.h"
+#include "constants.h"
+
+namespace bf = boost::filesystem;
+
+using namespace appimage::core;
+using namespace appimage::utils;
+using namespace XdgUtils::DesktopEntry;
+
+namespace appimage {
+    namespace desktop_integration {
+        namespace integrator {
+            /**
+             * Implementation of the opaque pointer patter for the integrator class
+             * see https://en.wikipedia.org/wiki/Opaque_pointer
+             *
+             * Contain a set of helper methods that will be used at the integrator class to fulfill the different task
+             */
+            class Integrator::Priv {
+            public:
+                core::AppImage appImage;
+                bf::path xdgDataHome;
+                std::string appImageId;
+
+                ResourcesExtractor resourcesExtractor;
+                DesktopEntry desktopEntry;
+
+                Priv(const AppImage& appImage, const bf::path& xdgDataHome)
+                    : appImage(appImage), xdgDataHome(xdgDataHome),
+                      resourcesExtractor(appImage) {
+
+                    if (xdgDataHome.empty())
+                        throw DesktopIntegrationError("Invalid XDG_DATA_HOME: " + xdgDataHome.string());
+
+                    // Extract desktop entry, DesktopIntegrationError will be throw if missing
+                    auto desktopEntryPath = resourcesExtractor.getDesktopEntryPath();
+                    auto desktopEntryData = resourcesExtractor.extractText(desktopEntryPath);
+                    try {
+                        desktopEntry = std::move(DesktopEntry(desktopEntryData));
+                    } catch (const DesktopEntryError& error) {
+                        throw DesktopIntegrationError(std::string("Malformed desktop entry: ") + error.what());
+                    }
+
+
+                    appImageId = hashPath(appImage.getPath());
+                }
+
+                /**
+                 * Check if the AppImage author requested that this AppImage should not be integrated
+                 */
+                void assertItShouldBeIntegrated() {
+                    try {
+                        if (desktopEntry.exists("Desktop Entry/X-AppImage-Integrate")) {
+                            const auto integrationRequested = static_cast<bool>(desktopEntry["Desktop Entry/X-AppImage-Integrate"]);
+
+                            if (!integrationRequested)
+                                throw DesktopIntegrationError("The AppImage explicitly requested to not be integrated");
+                        }
+
+                        if (desktopEntry.exists("Desktop Entry/NoDisplay")) {
+                            const auto noDisplay = static_cast<bool>(desktopEntry["Desktop Entry/NoDisplay"]);
+
+                            if (noDisplay)
+                                throw DesktopIntegrationError("The AppImage explicitly requested to not be integrated");
+                        }
+                    } catch (const XdgUtils::DesktopEntry::BadCast& err) {
+                        // if the value is not a bool we can ignore it
+                        Logger::warning(err.what());
+                    }
+                }
+
+                void deployDesktopEntry() {
+                    bf::path desktopEntryDeployPath = buildDesktopFilePath();
+
+                    // ensure that the parent path exists
+                    create_directories(desktopEntryDeployPath.parent_path());
+
+                    // update references to the deployed resources
+                    DesktopEntry editedDesktopEntry = desktopEntry;
+                    editDesktopEntry(editedDesktopEntry, appImageId);
+
+                    // write file contents
+                    std::ofstream desktopEntryFile(desktopEntryDeployPath.string());
+                    desktopEntryFile << editedDesktopEntry;
+
+                    // make it executable (required by some desktop environments)
+                    bf::permissions(desktopEntryDeployPath, bf::owner_read | bf::owner_exe | bf::add_perms);
+                }
+
+                /**
+                 * @brief Build the file path were the AppImage desktop file should be copied in order to achieve the desktop
+                 * integration.
+                 *
+                 * The desktop file path is made by the following rule:
+                 * "$XDG_DATA_HOME/applications/<vendor id>_<appImageId>-<application name scaped>.desktop"
+                 * where:
+                 *  - vendor id = appimagekit
+                 *  - appImageId = appimage path md5 sum
+                 *  - application name escaped: the application name as in the Name entry at desktop file inside the
+                 *                              AppImage with spaces replaced by underscores
+                 *
+                 * @param resources
+                 * @return desktop file path
+                 */
+                std::string buildDesktopFilePath() const {
+                    // Get application name
+                    if (!desktopEntry.exists("Desktop Entry/Name"))
+                        throw DesktopIntegrationError("Desktop file does not contain Name entry");
+
+                    // we don't trust the application name inside the desktop file, so we sanitize the filename before
+                    // calculating the integrated icon's path
+                    // this keeps the filename understandable while mitigating risks for potential attacks
+                    std::string sanitizedName = desktopEntry.get("Desktop Entry/Name");
+                    boost::trim(sanitizedName);
+                    sanitizedName = StringSanitizer(sanitizedName).sanitizeForPath();
+
+                    // assemble the desktop file path
+                    std::string desktopFileName =
+                        VENDOR_PREFIX + "_" + appImageId + "-" + sanitizedName + ".desktop";
+                    bf::path expectedDesktopFilePath(xdgDataHome / "applications" / desktopFileName);
+
+                    return expectedDesktopFilePath.string();
+                }
+
+                /**
+                 * Set the desktop entry paths to their expected locations
+                 * @param entry
+                 * @param md5str
+                 */
+                void editDesktopEntry(DesktopEntry& entry, const std::string& md5str) const {
+                    // Prepare Desktop Entry editor
+                    DesktopEntryEditor editor;
+                    // Set the path used in the Exec and tryExec fields
+                    editor.setAppImagePath(appImage.getPath());
+                    // Set the identifier to be used while prefixing the icon files
+                    editor.setIdentifier(md5str);
+                    // Apply changes to the desktop entry
+                    editor.edit(entry);
+                }
+
+                /**
+                 * Find and deploy the AppImage icons resources.
+                 * Icons at usr/share/icons will be preferred if not available the ".DirIcon" will be used.
+                 */
+                void deployIcons() {
+                    static const std::string dirIconPath = ".DirIcon";
+                    static const auto iconsDirPath = "usr/share/icons";
+
+                    // get the name of the icon used in the desktop entry
+                    const std::string desktopEntryIconName = desktopEntry.get("Desktop Entry/Icon");
+
+                    if (desktopEntryIconName.empty())
+                        throw DesktopIntegrationError("Missing icon field in the desktop entry");
+
+                    // security check -- paths should not be attempted to be looked up, the desktop files can be
+                    // malicious
+                    if (desktopEntryIconName.find('/') != std::string::npos) {
+                        throw DesktopIntegrationError("Icon field contains path");
+                    }
+
+                    auto iconPaths = resourcesExtractor.getIconFilePaths(desktopEntryIconName);
+
+                    // If the main app icon is not usr/share/icons we should deploy the .DirIcon in its place
+                    if (iconPaths.empty()) {
+                        Logger::warning(std::string("No icons found at \"") + iconsDirPath + "\"");
+
+                        try {
+                            Logger::warning("Using .DirIcon as default app icon");
+                            auto dirIconData = resourcesExtractor.extract(dirIconPath);
+                            deployApplicationIcon(desktopEntryIconName, dirIconData);;
+                        } catch (const PayloadIteratorError& error) {
+                            Logger::error(error.what());
+                            Logger::error("No icon was generated for: " + appImage.getPath());
+                        }
+                    } else {
+                        // Generate the target paths were the Desktop Entry icons will be deployed
+                        std::map<std::string, std::string> iconFilesTargetPaths;
+                        for (const auto& itr: iconPaths)
+                            iconFilesTargetPaths[itr] = generateDeployPath(itr).string();
+
+                        resourcesExtractor.extractTo(iconFilesTargetPaths);
+                    }
+                }
+
+                /**
+                 * Deploy <iconData> as the main application icon to
+                 * XDG_DATA_HOME/icons/hicolor/<size>/apps/<vendorPrefix>_<appImageId>_<iconName>.<format extension>
+                 *
+                 * size: actual icon dimenzions, in case of vectorial image "scalable" is used
+                 * format extension: in case of vectorial image "svg" otherwise "png"
+                 *
+                 * @param iconName
+                 * @param iconData
+                 */
+                void deployApplicationIcon(const std::string& iconName, std::vector<char>& iconData) const {
+                    try {
+                        IconHandle icon(iconData);
+
+                        // build the icon path and name attending to its format and size as
+                        // icons/hicolor/<size>/apps/<vendorPrefix>_<appImageId>_<iconName>.<format extension>
+                        boost::filesystem::path iconPath = "icons/hicolor";
+
+                        std::stringstream iconNameBuilder;
+
+                        // we don't trust the icon name inside the desktop file, so we sanitize the filename before
+                        // calculating the integrated icon's path
+                        // this keeps the filename understandable while mitigating risks for potential attacks
+                        iconNameBuilder << StringSanitizer(iconName).sanitizeForPath();
+
+                        // in case of vectorial images use ".svg" as extension and "scalable" as size
+                        if (icon.format() == "svg") {
+                            iconNameBuilder << ".svg";
+                            iconPath /= "scalable";
+                        } else {
+                            // otherwise use "png" as extension and the actual icon size as size
+                            iconNameBuilder << ".png";
+
+                            auto iconSize = std::to_string(icon.getSize());
+                            iconPath /= (iconSize + "x" + iconSize);
+                        }
+
+                        iconPath /= "apps";
+                        iconPath /= iconNameBuilder.str();
+
+                        auto deployPath = generateDeployPath(iconPath);
+                        icon.save(deployPath.string(), icon.format());
+                    } catch (const IconHandleError& er) {
+                        Logger::error(er.what());
+                        Logger::error("No icon was generated for: " + appImage.getPath());
+                    }
+                }
+
+                /**
+                 * Append vendor prefix and appImage id to the file names to identify the appImage that owns
+                 * this file. Replace the default XDG_DATA_DIR by the one at <xdgDataHome>
+                 *
+                 * @param path resource path
+                 * @return path with a prefixed file name
+                 */
+                bf::path generateDeployPath(bf::path path) const {
+                    // add appImage resource identification prefix to the filename
+                    std::stringstream fileNameBuilder;
+                    fileNameBuilder << VENDOR_PREFIX << "_" << appImageId << "_" << path.filename().string();
+
+                    // build the relative parent path ignoring the default XDG_DATA_DIR prefix ("usr/share")
+                    path.remove_filename();
+                    bf::path relativeParentPath;
+                    const bf::path defaultXdgDataDirPath = "usr/share";
+
+                    for (const auto& itr : path) {
+                        relativeParentPath /= itr;
+
+                        if (relativeParentPath == defaultXdgDataDirPath)
+                            relativeParentPath.clear();
+                    }
+
+                    bf::path newPath = xdgDataHome / relativeParentPath / fileNameBuilder.str();
+                    return newPath;
+                }
+
+                void deployMimeTypePackages() {
+                    const auto mimeTypePackagesPaths = resourcesExtractor.getMimeTypePackagesPaths();
+                    std::map<std::string, std::string> mimeTypePackagesTargetPaths;
+
+                    // Generate deploy paths
+                    for (const auto& path: mimeTypePackagesPaths) {
+                        const auto deploymentPath =  generateDeployPath(path).string();
+                        mimeTypePackagesTargetPaths[path] = deploymentPath;
+                    }
+
+                    resourcesExtractor.extractTo(mimeTypePackagesTargetPaths);
+                }
+
+                void setExecutionPermission() {
+                    if (access(appImage.getPath().c_str(), X_OK) != F_OK)
+                        try {
+                            bf::permissions(appImage.getPath(), bf::owner_read | bf::owner_exe |
+                                                                bf::group_read | bf::group_exe |
+                                                                bf::others_read | bf::others_exe |
+                                                                bf::add_perms);
+                        } catch (const bf::filesystem_error&) {
+                            Logger::error("Unable to set execution permissions on " + appImage.getPath());
+                        }
+                }
+            };
+
+            Integrator::Integrator(const AppImage& appImage, const bf::path& xdgDataHome)
+                : d(new Priv(appImage, xdgDataHome)) {}
+
+            Integrator::~Integrator() = default;
+
+            void Integrator::integrate() {
+                // an unedited desktop entry is required to identify the resources to be deployed
+                d->assertItShouldBeIntegrated();
+
+                // Must be executed before deployDesktopEntry because it changes the icon names
+                d->deployIcons();
+                d->deployDesktopEntry();
+                d->deployMimeTypePackages();
+                d->setExecutionPermission();
+            }
+        }
+    }
+}
diff --git a/src/libappimage/desktop_integration/integrator/Integrator.h b/src/libappimage/desktop_integration/integrator/Integrator.h
new file mode 100644 (file)
index 0000000..835c40a
--- /dev/null
@@ -0,0 +1,52 @@
+#pragma once
+
+// system
+#include <memory>
+#include <string>
+
+// local
+#include <appimage/core/AppImage.h>
+#include "constants.h"
+
+namespace appimage {
+    namespace desktop_integration {
+        namespace integrator {
+            /**
+             * @brief Integrator instances allow the integration and disintegration of AppImage with XDG compliant desktop
+             * environments.
+             */
+            class Integrator {
+            public:
+
+                /**
+                 * Create an Integrator instance with a custom XDG_DATA_HOME.
+                 * @param appImage
+                 * @param xdgDataHome
+                 */
+                explicit Integrator(const core::AppImage& appImage, const boost::filesystem::path& xdgDataHome);
+
+                // Creating copies of this object is not allowed
+                Integrator(Integrator& other) = delete;
+
+                // Creating copies of this object is not allowed
+                Integrator& operator=(Integrator& other) = delete;
+
+                virtual ~Integrator();
+
+                /**
+                 * @brief Perform the AppImage integration into the Desktop Environment
+                 *
+                 * Extract the main application desktop entry, icons and mime type packages. Modifies their content to
+                 * properly match the AppImage file location and deploy them into the use XDG_DATA_HOME appending a
+                 * prefix to each file. Such prefix is composed as "<vendor id>_<appimage_path_md5>_<old_file_name>"
+                 */
+                void integrate();
+
+            private:
+                class Priv;
+                std::unique_ptr<Priv> d;   // opaque pointer
+            };
+        }
+
+    }
+}
diff --git a/src/libappimage/libappimage.c b/src/libappimage/libappimage.c
new file mode 100644 (file)
index 0000000..85d935c
--- /dev/null
@@ -0,0 +1,92 @@
+/**************************************************************************
+ *
+ * Copyright (c) 2004-18 Simon Peter
+ *
+ * All Rights Reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ **************************************************************************/
+
+#ident "AppImage by Simon Peter, http://appimage.org/"
+
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <dirent.h>
+
+#include "xdg-basedir.h"
+
+// own header
+#include "appimage/appimage.h"
+
+#if HAVE_LIBARCHIVE3 == 1 // CentOS
+# include <archive3.h>
+# include <archive_entry3.h>
+#else // other systems
+
+# include <archive.h>
+# include <archive_entry.h>
+#endif
+
+#include <glob.h>
+#include <appimage/appimage_legacy.h>
+
+char* appimage_registered_desktop_file_path(const char* path, char* md5, bool verbose) {
+    glob_t pglob = {};
+
+    // if md5 has been calculated before, we can just use it to save these extra calculations
+    // if not, we need to calculate it here
+    if (md5 == NULL)
+        md5 = appimage_get_md5(path);
+
+    // sanity check
+    if (md5 == NULL) {
+        if (verbose)
+            fprintf(stderr, "appimage_get_md5() failed\n");
+        return NULL;
+    }
+
+    char* data_home = xdg_data_home();
+
+    // TODO: calculate this value exactly
+    char* glob_pattern = malloc(PATH_MAX);
+    sprintf(glob_pattern, "%s/applications/appimagekit_%s-*.desktop", data_home, md5);
+
+    glob(glob_pattern, 0, NULL, &pglob);
+
+    char* rv = NULL;
+
+    if (pglob.gl_pathc <= 0) {
+        if (verbose) {
+            fprintf(stderr, "No results found by glob()");
+        }
+    } else if (pglob.gl_pathc >= 1) {
+        if (pglob.gl_pathc > 1 && verbose) {
+            fprintf(stderr, "Too many results returned by glob(), returning first result found");
+        }
+
+        // need to copy value to be able to globfree() later on
+        rv = strdup(pglob.gl_pathv[0]);
+    }
+
+    globfree(&pglob);
+
+    return rv;
+};
diff --git a/src/libappimage/libappimage.cpp b/src/libappimage/libappimage.cpp
new file mode 100644 (file)
index 0000000..f4c4893
--- /dev/null
@@ -0,0 +1,344 @@
+/**
+ * Implementation of the C interface functions
+ */
+// system
+#include <cstring>
+#include <sstream>
+
+//for std::underlying_type
+#include <type_traits>
+
+// libraries
+#include <boost/filesystem.hpp>
+#include <boost/algorithm/string.hpp>
+
+// local
+#include <XdgUtils/DesktopEntry/DesktopEntry.h>
+#include <appimage/utils/ResourcesExtractor.h>
+#include <appimage/core/AppImage.h>
+#include <appimage/config.h>
+#include "utils/Logger.h"
+#include "utils/hashlib.h"
+#include "utils/UrlEncoder.h"
+#include "utils/path_utils.h"
+
+#ifdef LIBAPPIMAGE_DESKTOP_INTEGRATION_ENABLED
+#include <appimage/desktop_integration/IntegrationManager.h>
+#include <appimage/appimage.h>
+#endif
+
+using namespace appimage::core;
+using namespace appimage::utils;
+
+namespace bf = boost::filesystem;
+
+/**
+ * We cannot allow any exception to scape from C++ to C. This will wrap a given code block into a try-catch
+ * structure and will effectively catch and log all exceptions.
+ *
+ * @param src
+ */
+#define CATCH_ALL(src) { \
+    try { \
+        src \
+    }  catch (const std::runtime_error& err) { \
+        Logger::error(std::string(__FUNCTION__) + " : " + err.what()); \
+    } catch (...) { \
+        Logger::error(std::string(__FUNCTION__) + " : " + " unexpected error"); \
+    } \
+}
+
+extern "C" {
+
+
+/* Check if a file is an AppImage. Returns the image type if it is, or -1 if it isn't */
+int appimage_get_type(const char* path, bool) {
+    typedef std::underlying_type<AppImageFormat>::type utype;
+    CATCH_ALL(
+        const auto format = AppImage::getFormat(path);
+        return static_cast<utype>(format);
+    );
+    return static_cast<utype>(AppImageFormat::INVALID);
+}
+
+char** appimage_list_files(const char* path) {
+    char** result = nullptr;
+    CATCH_ALL(
+        AppImage appImage(path);
+
+        std::vector<std::string> files;
+        for (auto itr = appImage.files(); itr != itr.end(); ++itr)
+            if (!(*itr).empty())
+                files.emplace_back(*itr);
+
+
+        result = static_cast<char**>(malloc(sizeof(char*) * (files.size() + 1)));
+        for (int i = 0; i < files.size(); i++)
+            result[i] = strdup(files[i].c_str());
+
+        result[files.size()] = nullptr;
+
+        return result;
+    );
+
+    // Create empty string list
+    result = static_cast<char**>(malloc(sizeof(char*)));
+    result[0] = nullptr;
+
+    return result;
+}
+
+void appimage_string_list_free(char** list) {
+    for (char** ptr = list; ptr != NULL && *ptr != NULL; ptr++)
+        free(*ptr);
+
+    free(list);
+}
+
+bool
+appimage_read_file_into_buffer_following_symlinks(const char* appimage_file_path, const char* file_path, char** buffer,
+                                                  unsigned long* buf_size) {
+    // Init output params
+    *buffer = nullptr;
+    *buf_size = 0;
+
+    CATCH_ALL(
+        AppImage appImage(appimage_file_path);
+        appimage::utils::ResourcesExtractor resourcesExtractor(appImage);
+
+        auto fileData = resourcesExtractor.extract(file_path);
+
+        *buffer = static_cast<char*>(malloc(sizeof(char) * fileData.size()));
+        std::copy(fileData.begin(), fileData.end(), *buffer);
+
+        *buf_size = fileData.size();
+
+        return true;
+    );
+
+    return false;
+}
+
+void appimage_extract_file_following_symlinks(const char* appimage_file_path, const char* file_path,
+                                              const char* target_file_path) {
+    CATCH_ALL(
+        AppImage appImage(appimage_file_path);
+        appimage::utils::ResourcesExtractor resourcesExtractor(appImage);
+
+        resourcesExtractor.extractTo({{file_path, target_file_path}});
+    );
+}
+
+
+/*
+ * Checks whether an AppImage's desktop file has set X-AppImage-Integrate=false or NoDisplay=true.
+ * Useful to check whether the author of an AppImage doesn't want it to be integrated.
+ *
+ * Returns >0 if set, 0 if not set, <0 on errors.
+ */
+int appimage_shall_not_be_integrated(const char* path) {
+    CATCH_ALL(
+        AppImage appImage(path);
+        XdgUtils::DesktopEntry::DesktopEntry entry;
+
+        // Load Desktop Entry
+        for (auto itr = appImage.files(); itr != itr.end(); ++itr) {
+            const auto& entryPath = *itr;
+            if (entryPath.find(".desktop") != std::string::npos && entryPath.find('/') == std::string::npos) {
+                // use the resources extractor to make sure symlinks are resolved
+                ResourcesExtractor extractor(appImage);
+
+                const auto contents = extractor.extractText(entryPath);
+
+                // empty desktop files are clearly an error
+                if (contents.empty()) {
+                    return -1;
+                }
+
+                entry = std::move(XdgUtils::DesktopEntry::DesktopEntry(contents));
+
+                break;
+            }
+        }
+
+        {
+            auto integrateEntryValue = entry.get("Desktop Entry/X-AppImage-Integrate", "true");
+
+            boost::to_lower(integrateEntryValue);
+            boost::algorithm::trim(integrateEntryValue);
+
+            if (integrateEntryValue == "false") {
+                return 1;
+            }
+        }
+
+        {
+            auto noDisplayValue = entry.get("Desktop Entry/NoDisplay", "false");
+
+            boost::to_lower(noDisplayValue);
+            boost::algorithm::trim(noDisplayValue);
+
+            if (noDisplayValue == "true") {
+                return 1;
+            }
+        }
+
+        return 0;
+    )
+
+    return -1;
+}
+
+
+/*
+ * Checks whether an AppImage's desktop file has set Terminal=true.
+ * Useful to check whether the author of an AppImage doesn't want it to be integrated.
+ *
+ * Returns >0 if set, 0 if not set, <0 on errors.
+ */
+int appimage_is_terminal_app(const char* path) {
+    CATCH_ALL(
+        AppImage appImage(path);
+
+        std::vector<char> data;
+
+        XdgUtils::DesktopEntry::DesktopEntry entry;
+
+        // Load Desktop Entry
+        for (auto itr = appImage.files(); itr != itr.end(); ++itr) {
+            const auto& entryPath = *itr;
+            if (entryPath.find(".desktop") != std::string::npos && entryPath.find('/') == std::string::npos) {
+                // use the resources extractor to make sure symlinks are resolved
+                ResourcesExtractor extractor(appImage);
+
+                const auto contents = extractor.extractText(entryPath);
+
+                // empty desktop files are clearly an error
+                if (contents.empty()) {
+                    return -1;
+                }
+
+                entry = std::move(XdgUtils::DesktopEntry::DesktopEntry(contents));
+
+                break;
+            }
+        }
+
+        auto terminalEntryValue = entry.get("Desktop Entry/Terminal", "false");
+
+        boost::to_lower(terminalEntryValue);
+        boost::algorithm::trim(terminalEntryValue);
+
+        return terminalEntryValue == "true";
+    );
+
+    return -1;
+}
+
+
+/* Return the md5 hash constructed according to
+ * https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html#THUMBSAVE
+ * This can be used to identify files that are related to a given AppImage at a given location */
+char* appimage_get_md5(const char* path) {
+    using namespace appimage::utils;
+
+    if (path == nullptr)
+        return nullptr;
+
+    CATCH_ALL(
+        auto hash = hashPath(path);
+        if (hash.empty())
+            return nullptr;
+        else
+            return strdup(hash.c_str());
+    );
+
+    return nullptr;
+}
+
+
+off_t appimage_get_payload_offset(char const* path) {
+    if (path == nullptr)
+        return 0;
+
+    CATCH_ALL(
+        return AppImage(path).getPayloadOffset();
+    );
+
+    return 0;
+}
+
+#ifdef LIBAPPIMAGE_DESKTOP_INTEGRATION_ENABLED
+using namespace appimage::desktop_integration;
+
+/*
+ * Register an AppImage in the system
+ * Returns 0 on success, non-0 otherwise.
+ */
+int appimage_register_in_system(const char* path, bool verbose) {
+    CATCH_ALL(
+        AppImage appImage(path);
+        IntegrationManager manager;
+        manager.registerAppImage(appImage);
+
+#ifdef LIBAPPIMAGE_THUMBNAILER_ENABLED
+        manager.generateThumbnails(appImage);
+#endif // LIBAPPIMAGE_THUMBNAILER_ENABLED
+        return 0;
+    );
+
+    return 1;
+}
+
+
+/* Unregister an AppImage in the system */
+int appimage_unregister_in_system(const char* path, bool verbose) {
+    if (path == nullptr)
+        return 1;
+
+    CATCH_ALL(
+        IntegrationManager manager;
+        manager.unregisterAppImage(path);
+
+#ifdef LIBAPPIMAGE_THUMBNAILER_ENABLED
+        manager.removeThumbnails(path);
+#endif // LIBAPPIMAGE_THUMBNAILER_ENABLED
+        return 0;
+    );
+
+    return 1;
+}
+
+/* Check whether AppImage is registered in the system already */
+bool appimage_is_registered_in_system(const char* path) {
+    if (path == nullptr)
+        return false;
+
+    CATCH_ALL(
+        IntegrationManager manager;
+        return manager.isARegisteredAppImage(path);
+    );
+
+    return false;
+}
+
+
+#ifdef LIBAPPIMAGE_THUMBNAILER_ENABLED
+/* Create AppImage thumbanil according to
+ * https://specifications.freedesktop.org/thumbnail-spec/0.8.0/index.html
+ */
+bool appimage_create_thumbnail(const char* appimage_file_path, bool verbose) {
+    CATCH_ALL(
+        AppImage appImage(appimage_file_path);
+        IntegrationManager manager;
+        manager.generateThumbnails(appImage);
+        return true;
+    );
+
+    return false;
+}
+
+#endif // LIBAPPIMAGE_THUMBNAILER_ENABLED
+#endif // LIBAPPIMAGE_DESKTOP_INTEGRATION_ENABLED
+
+}
diff --git a/src/libappimage/libappimage_legacy.cpp b/src/libappimage/libappimage_legacy.cpp
new file mode 100644 (file)
index 0000000..9448934
--- /dev/null
@@ -0,0 +1,41 @@
+// local
+#include <appimage/appimage.h>
+
+extern "C" {
+ssize_t appimage_get_elf_size(const char* fname) {
+    return appimage_get_payload_offset(fname);
+}
+
+int appimage_type1_is_terminal_app(const char* path) {
+    return appimage_is_terminal_app(path);
+} ;
+
+int appimage_type2_is_terminal_app(const char* path) {
+    return appimage_is_terminal_app(path);
+} ;
+
+#ifdef LIBAPPIMAGE_DESKTOP_INTEGRATION_ENABLED
+/* Register a type 1 AppImage in the system
+ * DEPRECATED, it should be removed ASAP
+ * */
+bool appimage_type1_register_in_system(const char* path, bool verbose) {
+    return appimage_register_in_system(path, verbose) == 0;
+}
+
+/* Register a type 2 AppImage in the system
+ * DEPRECATED it should be removed ASAP
+ * */
+bool appimage_type2_register_in_system(const char* path, bool verbose) {
+    return appimage_register_in_system(path, verbose) == 0;
+}
+
+int appimage_type1_shall_not_be_integrated(const char* path) {
+    return appimage_shall_not_be_integrated(path);
+}
+
+int appimage_type2_shall_not_be_integrated(const char* path) {
+    return appimage_shall_not_be_integrated(path);
+}
+
+#endif // LIBAPPIMAGE_DESKTOP_INTEGRATION_ENABLED
+}
diff --git a/src/libappimage/type1.c b/src/libappimage/type1.c
new file mode 100644 (file)
index 0000000..1b09f00
--- /dev/null
@@ -0,0 +1,205 @@
+// system includes
+#include <fcntl.h>
+
+// library includes
+#include <archive.h>
+#include <archive_entry.h>
+
+// local includes
+#include "type2.h"
+#include "type1.h"
+
+
+void appimage_type1_open(appimage_handler* handler) {
+    if (is_handler_valid(handler) && !handler->is_open) {
+#ifdef STANDALONE
+        fprintf(stderr, "Opening %s as Type 1 AppImage\n", handler->path);
+#endif
+        struct archive* a;
+        a = archive_read_new();
+        archive_read_support_format_iso9660(a);
+        if (archive_read_open_filename(a, handler->path, 10240) != ARCHIVE_OK) {
+            fprintf(stderr, "%s", archive_error_string(a));
+            handler->cache = NULL;
+            handler->is_open = false;
+        } else {
+            handler->cache = a;
+            handler->is_open = true;
+        }
+    }
+}
+
+void appimage_type1_close(appimage_handler* handler) {
+    if (is_handler_valid(handler) && handler->is_open) {
+#ifdef STANDALONE
+        fprintf(stderr, "Closing %s\n", handler->path);
+#endif
+        struct archive* a = handler->cache;
+        archive_read_close(a);
+        archive_read_free(a);
+
+        handler->cache = NULL;
+        handler->is_open = false;
+    }
+}
+
+void type1_traverse(appimage_handler* handler, traverse_cb command, void* command_data) {
+    appimage_type1_open(handler);
+
+    if (!command) {
+#ifdef STANDALONE
+        fprintf(stderr, "No traverse command set.\n");
+#endif
+        return;
+    }
+
+    if (handler->is_open) {
+        struct archive* a = handler->cache;
+        struct archive_entry* entry;
+        int r;
+
+        for (;;) {
+            r = archive_read_next_header(a, &entry);
+            if (r == ARCHIVE_EOF) {
+                break;
+            }
+            if (r != ARCHIVE_OK) {
+                fprintf(stderr, "%s\n", archive_error_string(a));
+                break;
+            }
+
+            /* Skip all but regular files; FIXME: Also handle symlinks correctly */
+            if (archive_entry_filetype(entry) != AE_IFREG) {
+                continue;
+            }
+
+            command(handler, entry, command_data);
+        }
+    }
+
+    appimage_type1_close(handler);
+}
+
+// TODO: remove forward declaration
+gchar* replace_str(const gchar* src, const gchar* find, const gchar* replace);
+
+char* type1_get_file_name(appimage_handler* handler, void* data) {
+    (void) handler;
+
+    struct archive_entry* entry = (struct archive_entry*) data;
+
+    char* filename = replace_str(archive_entry_pathname(entry), "./", "");
+    return filename;
+}
+
+void type1_extract_file(appimage_handler* handler, void* data, const char* target) {
+    (void) data;
+
+    struct archive* a = handler->cache;
+    mk_base_dir(target);
+
+    mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
+    int f = open(target, O_WRONLY | O_CREAT | O_TRUNC, mode);
+
+    if (f == -1){
+#ifdef STANDALONE
+        fprintf(stderr, "open error: %s\n", target);
+#endif
+        return;
+    }
+
+    archive_read_data_into_fd(a, f);
+    close(f);
+}
+
+bool type1_read_file_into_buf(struct appimage_handler* handler, void* data, char** buffer, unsigned long* buf_size) {
+    (void) data;
+
+    struct archive* a = handler->cache;
+
+    struct archive_entry* entry = data;
+
+    int64_t file_size = archive_entry_size(entry);
+
+    char* new_buffer = (char*) malloc(sizeof(char) * file_size);
+
+    if (new_buffer == NULL) {
+#ifdef STANDALONE
+        fprintf(stderr, "failed to allocate enough memory for buffer (required: %ul bytes)\n", file_size);
+#endif
+        return false;
+    }
+
+    if (archive_read_data(a, new_buffer, (size_t) file_size) < 0) {
+#ifdef STANDALONE
+        fprintf(stderr, "failed to read data into buffer: %s\n", archive_error_string(a));
+#endif
+        free(new_buffer);
+        return false;
+    }
+
+    *buffer = new_buffer;
+    *buf_size = (unsigned long) file_size;
+    return true;
+}
+
+char* type1_get_file_link(struct appimage_handler* handler, void* entry_ptr) {
+    struct archive_entry* entry = entry_ptr;
+
+    const char* link_path = archive_entry_symlink(entry) ?: archive_entry_hardlink(entry);
+
+    if (link_path) {
+        char* filename = replace_str(link_path, "./", "");
+        return filename;
+    }
+
+    return NULL;
+}
+
+appimage_handler appimage_type_1_create_handler() {
+    appimage_handler h;
+    h.traverse = type1_traverse;
+    h.get_file_name = type1_get_file_name;
+    h.extract_file = type1_extract_file;
+    h.get_file_link = type1_get_file_link;
+    h.read_file_into_new_buffer = type1_read_file_into_buf;
+    h.type = 1;
+
+    return h;
+}
+
+bool match_type_1_magic_bytes(const char* buffer) {
+    return buffer[0] == 0x41 && buffer[1] == 0x49 && buffer[2] == 0x01;
+}
+
+bool is_iso_9660_file(const char* path) {
+    /* Implementation of the signature matches expressed at https://www.garykessler.net/library/file_sigs.html
+     * Signature: 43 44 30 30 31               = "CD001"
+     * ISO             ISO-9660 CD Disc Image
+     * This signature usually occurs at byte offset 32769 (0x8001),
+     * 34817 (0x8801), or 36865 (0x9001).
+     * More information can be found at MacTech or at ECMA.
+     */
+
+    bool res = false;
+    FILE* f = fopen(path, "rt");
+    if (f != NULL) {
+        char buffer[5] = {0};
+
+        int positions[] = {32769, 34817, 36865};
+        const char signature[] = "CD001";
+        for (int i = 0; i < 3 && !res; i++) {
+            int fseekRes = fseek(f, positions[i], SEEK_SET);
+            if (!fseekRes) {
+                fread(buffer, 1, 5, f);
+                int strCmpRes = memcmp(signature, buffer, 5);
+                if (!strCmpRes)
+                    res = true;
+            }
+            memset(buffer, 0, 5);
+        }
+
+        fclose(f);
+    }
+    return res;
+}
diff --git a/src/libappimage/type2.c b/src/libappimage/type2.c
new file mode 100644 (file)
index 0000000..ce32112
--- /dev/null
@@ -0,0 +1,250 @@
+// library includes
+#include <appimage/appimage.h>
+#include <squashfuse.h>
+#include <squashfs_fs.h>
+
+// local includes
+#include "type2.h"
+
+void appimage_type2_open(appimage_handler* handler) {
+    if (is_handler_valid(handler) && !handler->is_open) {
+#ifdef STANDALONE
+        fprintf(stderr, "Opening %s as Type 2 AppImage\n", handler->path);
+#endif
+        // The offset at which a squashfs image is expected
+        ssize_t fs_offset = appimage_get_elf_size(handler->path);
+
+        if (fs_offset < 0) {
+#ifdef STANDALONE
+            fprintf(stderr, "get_elf_size error\n");
+#endif
+            handler->is_open = false;
+            handler->cache = NULL;
+            return;
+        }
+
+        sqfs* fs = malloc(sizeof(sqfs));
+        sqfs_err err = sqfs_open_image(fs, handler->path, (size_t) fs_offset);
+        if (err != SQFS_OK) {
+#ifdef STANDALONE
+            fprintf(stderr, "sqfs_open_image error: %s\n", handler->path);
+#endif
+            free(fs);
+            handler->is_open = false;
+            handler->cache = NULL;
+        } else {
+            handler->is_open = true;
+            handler->cache = fs;
+        }
+    }
+}
+
+void appimage_type2_close(appimage_handler* handler) {
+    if (is_handler_valid(handler) && handler->is_open) {
+#ifdef STANDALONE
+        fprintf(stderr, "Closing %s\n", handler->path);
+#endif
+
+        sqfs_destroy(handler->cache);
+        free(handler->cache);
+
+        handler->is_open = false;
+        handler->cache = NULL;
+    }
+}
+
+// forward declaration, see below
+void appimage_type2_extract_symlink(sqfs* fs, sqfs_inode* inode, const char* target);
+// TODO: get rid of this forward declaration
+void squash_extract_inode_to_file(sqfs *fs, sqfs_inode *inode, const gchar *dest);
+
+void appimage_type2_extract_regular_file(sqfs* fs, sqfs_inode* inode, const char* target) {
+    mk_base_dir(target);
+
+    // Read the file in chunks
+    squash_extract_inode_to_file(fs, inode, target);
+}
+
+bool appimage_type2_resolve_symlink(sqfs* fs, sqfs_inode* inode) {
+    // no need to do anything if the passed inode is not a symlink
+    if (inode->base.inode_type != SQUASHFS_SYMLINK_TYPE)
+        return true;
+
+    // read twice: once to populate size to be able to allocate the right amount of memory, then to populate the buffer
+    size_t size;
+    sqfs_readlink(fs, inode, NULL, &size);
+
+    char buf[size];
+    int ret = sqfs_readlink(fs, inode, buf, &size);
+
+    if (ret != 0) {
+#ifdef STANDALONE
+        fprintf(stderr, "WARNING: Symlink error.");
+#endif
+        return false;
+    }
+
+    sqfs_err err = sqfs_inode_get(fs, inode, fs->sb.root_inode);
+    if (err != SQFS_OK) {
+#ifdef STANDALONE
+        fprintf(stderr, "WARNING: Unable to get the root inode. Error: %d", err);
+#endif
+        return false;
+    }
+
+    bool found = false;
+    err = sqfs_lookup_path(fs, inode, buf, &found);
+    if (err != SQFS_OK) {
+#ifdef STANDALONE
+        fprintf(stderr, "WARNING: There was an error while trying to lookup a symblink. Error: %d", err);
+#endif
+        return false;
+    }
+
+    return true;
+}
+
+bool appimage_type2_extract_file_following_symlinks(sqfs* fs, sqfs_inode* inode, const char* target) {
+    if (!appimage_type2_resolve_symlink(fs, inode)) {
+#ifdef STANDALONE
+        fprintf(stderr, "ERROR: Failed to resolve symlink");
+#endif
+        return false;
+    }
+
+    if (inode->base.inode_type != SQUASHFS_REG_TYPE && inode->base.inode_type != SQUASHFS_LREG_TYPE) {
+#ifdef STANDALONE
+        fprintf(stderr, "WARNING: Unable to extract file of type %d", inode->base.inode_type);
+#endif
+        return false;
+    }
+
+    appimage_type2_extract_regular_file(fs, inode, target);
+    return true;
+}
+
+void type2_traverse(appimage_handler* handler, traverse_cb command, void* command_data) {
+    appimage_type2_open(handler);
+
+    if (handler->is_open && handler->cache != NULL) {
+        sqfs* fs = handler->cache;
+        sqfs_traverse trv;
+        sqfs_inode_id root_inode = sqfs_inode_root(fs);
+        sqfs_err err = sqfs_traverse_open(&trv, fs, root_inode);
+        if (err != SQFS_OK) {
+#ifdef STANDALONE
+            fprintf(stderr, "sqfs_traverse_open error\n");
+#endif
+        }
+        while (sqfs_traverse_next(&trv, &err))
+            command(handler, &trv, command_data);
+
+        if (err) {
+#ifdef STANDALONE
+            fprintf(stderr, "sqfs_traverse_next error\n");
+#endif
+        }
+        sqfs_traverse_close(&trv);
+    }
+
+    appimage_type2_close(handler);
+}
+
+char* type2_get_file_name(appimage_handler* handler, void* data) {
+    (void) handler;
+    sqfs_traverse* trv = data;
+    return strdup(trv->path);
+}
+
+void type2_extract_file(appimage_handler* handler, void* data, const char* target) {
+    sqfs* fs = handler->cache;
+    sqfs_traverse* trv = data;
+
+    sqfs_inode inode;
+    if (sqfs_inode_get(fs, &inode, trv->entry.inode)) {
+#ifdef STANDALONE
+        fprintf(stderr, "sqfs_inode_get error\n");
+#endif
+    }
+
+    appimage_type2_extract_file_following_symlinks(fs, &inode, target);
+}
+
+bool type2_read_file_into_buf(struct appimage_handler* handler, void* traverse, char** buffer, unsigned long* buf_size) {
+    sqfs* fs = handler->cache;
+    sqfs_traverse* trv = traverse;
+
+    sqfs_inode inode;
+    if (sqfs_inode_get(fs, &inode, trv->entry.inode)) {
+#ifdef STANDALONE
+        fprintf(stderr, "sqfs_inode_get error\n");
+#endif
+    }
+
+    // resolve symlink if possible
+    if (!appimage_type2_resolve_symlink(fs, &inode)) {
+#ifdef STANDALONE
+        fprintf(stderr, "ERROR: Failed to resolve symlink");
+#endif
+        return false;
+    }
+
+    if (inode.base.inode_type != SQUASHFS_REG_TYPE && inode.base.inode_type != SQUASHFS_LREG_TYPE) {
+#ifdef STANDALONE
+        fprintf(stderr, "WARNING: Unable to extract file of type %d", inode->base.inode_type);
+#endif
+        return false;
+    }
+
+    uint64_t file_size = inode.xtra.reg.file_size;
+
+    char* new_buffer = (char*) malloc(sizeof(char) * file_size);
+
+    if (new_buffer == NULL) {
+#ifdef STANDALONE
+        fprintf(stderr, "failed to allocate enough memory for buffer (required: %ul bytes)\n", file_size);
+#endif
+        return false;
+    }
+
+    if (sqfs_read_range(fs, &inode, 0, (sqfs_off_t*) &file_size, new_buffer) != SQFS_OK) {
+#ifdef STANDALONE
+        fprintf(stderr, "failed to read data into buffer\n");
+#endif
+        free(new_buffer);
+        return false;
+    }
+
+    *buffer = new_buffer;
+    *buf_size = file_size;
+    return true;
+}
+
+char* type2_get_file_link(struct appimage_handler* handler, void* data) {
+    sqfs_traverse* trv = data;
+
+    sqfs_inode inode;
+    if (!sqfs_inode_get(trv->fs, &inode, trv->entry.inode))
+        return NULL;
+
+    // read twice: once to populate size to be able to allocate the right amount of memory, then to populate the buffer
+    size_t size;
+    sqfs_readlink(trv->fs, &inode, NULL, &size);
+
+    char* buf = malloc(sizeof(char) * size);
+    int ret = sqfs_readlink(trv->fs, &inode, buf, &size);
+
+    return buf;
+}
+
+appimage_handler appimage_type_2_create_handler() {
+    appimage_handler h;
+    h.traverse = type2_traverse;
+    h.get_file_name = type2_get_file_name;
+    h.get_file_link = type2_get_file_link;
+    h.extract_file = type2_extract_file;
+    h.read_file_into_new_buffer = type2_read_file_into_buf;
+    h.type = 2;
+
+    return h;
+}
diff --git a/src/libappimage/utils/CMakeLists.txt b/src/libappimage/utils/CMakeLists.txt
new file mode 100644 (file)
index 0000000..c10c4c3
--- /dev/null
@@ -0,0 +1,32 @@
+set(
+    APPIMAGE_UTILS_SRCS
+    MagicBytesChecker.cpp
+    ElfFile.cpp
+    hashlib.cpp
+    UrlEncoder.cpp
+    IconHandle.cpp
+    Logger.cpp
+    path_utils.cpp
+    resources_extractor/ResourcesExtractor.cpp
+    resources_extractor/PayloadEntriesCache.cpp
+    StringSanitizer.cpp
+    StringSanitizer.h
+)
+
+set(APPIMAGE_UTILS_SRCS ${APPIMAGE_UTILS_SRCS} IconHandleCairoRsvg.cpp)
+add_library(appimage_utils OBJECT ${APPIMAGE_UTILS_SRCS})
+
+# Generic module configuration
+configure_libappimage_module(appimage_utils)
+target_include_directories(
+    appimage_utils
+    PRIVATE $<TARGET_PROPERTY:Boost::filesystem,INTERFACE_INCLUDE_DIRECTORIES>
+    PRIVATE $<TARGET_PROPERTY:XdgUtils::DesktopEntry,INTERFACE_INCLUDE_DIRECTORIES>
+    PRIVATE $<TARGET_PROPERTY:XdgUtils::BaseDir,INTERFACE_INCLUDE_DIRECTORIES>
+    PUBLIC $<TARGET_PROPERTY:libappimage_hashlib,INTERFACE_INCLUDE_DIRECTORIES>
+    PUBLIC $<TARGET_PROPERTY:libcairo,INTERFACE_INCLUDE_DIRECTORIES>
+    PUBLIC $<TARGET_PROPERTY:librsvg,INTERFACE_INCLUDE_DIRECTORIES>
+)
+target_include_directories(appimage_utils PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
+add_dependencies(appimage_utils Boost::filesystem libappimage_hashlib XdgUtils::DesktopEntry XdgUtils::BaseDir)
+target_compile_definitions(appimage_utils PUBLIC ${IMAGE_MANIPULATION_BACKEND_COMPILE_DEFINITIONS})
diff --git a/src/libappimage/utils/DLHandle.h b/src/libappimage/utils/DLHandle.h
new file mode 100644 (file)
index 0000000..86942ff
--- /dev/null
@@ -0,0 +1,77 @@
+#pragma once
+
+// system
+#include <string>
+#include <stdexcept>
+#include <dlfcn.h>
+
+namespace appimage {
+    namespace utils {
+        class DLHandleError : public std::runtime_error {
+        public:
+            explicit DLHandleError(const std::string& what) : runtime_error(what) {}
+        };
+
+        /**
+         * @brief Dynamic Loader wrapper
+         *
+         * Allow to dynamically load a library.
+         */
+        class DLHandle {
+        public:
+            /**
+             * Load <libName> with the given <mode> flags. For details about he allowed flags
+             * see the dlopen doc.
+             * @param libName
+             * @param mode
+             */
+            explicit DLHandle(const std::string& libName, int mode) : handle(nullptr), libName(libName) {
+                handle = dlopen(libName.c_str(), mode);
+
+                if (handle == nullptr)
+                    throw DLHandleError("Unable to load " + libName);
+            }
+
+            /**
+             * Load one of the libraries listed in <libNames> with the given <mode> flags. For details about
+             * the allowed flags see the dlopen doc.
+             * @param libName
+             * @param mode
+             */
+            explicit DLHandle(std::initializer_list<const std::string> libNames, int mode) : handle(nullptr) {
+                for (const auto& item: libNames) {
+                    handle = dlopen(item.c_str(), mode);
+                    if (handle != nullptr) {
+                        libName = item;
+                        break;
+                    }
+                }
+
+                if (handle == nullptr) {
+                    std::string libNamesStr;
+                    for (const auto& item: libNames)
+                        libNamesStr += " " + item;
+
+                    throw DLHandleError("Unable to load any of: " + libNamesStr);
+                }
+            }
+
+            virtual ~DLHandle() {
+                dlclose(handle);
+            };
+
+            template<typename T>
+            void loadSymbol(T& symbol, const std::string& symbolName) {
+                symbol = (T) dlsym(handle, symbolName.c_str());
+
+                if (symbol == nullptr)
+                    throw DLHandleError("Unable to load " + libName + " symbol: " + symbolName);
+            }
+
+        private:
+            std::string libName;
+            void* handle{};
+        };
+    }
+}
+
diff --git a/src/libappimage/utils/ElfFile.cpp b/src/libappimage/utils/ElfFile.cpp
new file mode 100644 (file)
index 0000000..724019c
--- /dev/null
@@ -0,0 +1,145 @@
+// system
+extern "C" {
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/mman.h>
+#include <sys/types.h>
+}
+
+// local
+#include "light_byteswap.h"
+#include "ElfFile.h"
+
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+#define ELFDATANATIVE ELFDATA2LSB
+#elif __BYTE_ORDER == __BIG_ENDIAN
+#define ELFDATANATIVE ELFDATA2MSB
+#else
+#error "Unknown machine endian"
+#endif
+
+namespace appimage {
+    namespace utils {
+        ElfFile::ElfFile(const std::string& path) : path(path), fname(path.c_str()), ehdr({0x0}) {}
+
+        uint16_t ElfFile::file16_to_cpu(uint16_t val) {
+            if (ehdr.e_ident[EI_DATA] != ELFDATANATIVE)
+                val = bswap_16(val);
+            return val;
+        }
+
+        uint32_t ElfFile::file32_to_cpu(uint32_t val) {
+            if (ehdr.e_ident[EI_DATA] != ELFDATANATIVE)
+                val = bswap_32(val);
+            return val;
+        }
+
+        uint64_t ElfFile::file64_to_cpu(uint64_t val) {
+            if (ehdr.e_ident[EI_DATA] != ELFDATANATIVE)
+                val = bswap_64(val);
+            return val;
+        }
+
+        off_t ElfFile::read_elf32(FILE* fd) {
+            Elf32_Ehdr ehdr32;
+            Elf32_Shdr shdr32;
+            off_t last_shdr_offset;
+            ssize_t ret;
+            off_t sht_end, last_section_end;
+
+            fseeko(fd, 0, SEEK_SET);
+            ret = fread(&ehdr32, 1, sizeof(ehdr32), fd);
+            if (ret < 0 || (size_t) ret != sizeof(ehdr32)) {
+                fprintf(stderr, "Read of ELF header from %s failed: %s\n",
+                        fname, strerror(errno));
+                return -1;
+            }
+
+            ehdr.e_shoff = file32_to_cpu(ehdr32.e_shoff);
+            ehdr.e_shentsize = file16_to_cpu(ehdr32.e_shentsize);
+            ehdr.e_shnum = file16_to_cpu(ehdr32.e_shnum);
+
+            last_shdr_offset = ehdr.e_shoff + (ehdr.e_shentsize * (ehdr.e_shnum - 1));
+            fseeko(fd, last_shdr_offset, SEEK_SET);
+            ret = fread(&shdr32, 1, sizeof(shdr32), fd);
+            if (ret < 0 || (size_t) ret != sizeof(shdr32)) {
+                fprintf(stderr, "Read of ELF section header from %s failed: %s\n",
+                        fname, strerror(errno));
+                return -1;
+            }
+
+            /* ELF ends either with the table of section headers (SHT) or with a section. */
+            sht_end = ehdr.e_shoff + (ehdr.e_shentsize * ehdr.e_shnum);
+            last_section_end = file64_to_cpu(shdr32.sh_offset) + file64_to_cpu(shdr32.sh_size);
+            return sht_end > last_section_end ? sht_end : last_section_end;
+        }
+
+        off_t ElfFile::read_elf64(FILE* fd) {
+            Elf64_Ehdr ehdr64;
+            Elf64_Shdr shdr64;
+            off_t last_shdr_offset;
+            off_t ret;
+            off_t sht_end, last_section_end;
+
+            fseeko(fd, 0, SEEK_SET);
+            ret = fread(&ehdr64, 1, sizeof(ehdr64), fd);
+            if (ret < 0 || (size_t) ret != sizeof(ehdr64)) {
+                fprintf(stderr, "Read of ELF header from %s failed: %s\n",
+                        fname, strerror(errno));
+                return -1;
+            }
+
+            ehdr.e_shoff = file64_to_cpu(ehdr64.e_shoff);
+            ehdr.e_shentsize = file16_to_cpu(ehdr64.e_shentsize);
+            ehdr.e_shnum = file16_to_cpu(ehdr64.e_shnum);
+
+            last_shdr_offset = ehdr.e_shoff + (ehdr.e_shentsize * (ehdr.e_shnum - 1));
+            fseeko(fd, last_shdr_offset, SEEK_SET);
+            ret = fread(&shdr64, 1, sizeof(shdr64), fd);
+            if (ret < 0 || ret != sizeof(shdr64)) {
+                Logger::error(std::string("Read of ELF section header from ") + fname
+                                             + " failed: " + strerror(errno));
+                return -1;
+            }
+
+            /* ELF ends either with the table of section headers (SHT) or with a section. */
+            sht_end = ehdr.e_shoff + (ehdr.e_shentsize * ehdr.e_shnum);
+            last_section_end = file64_to_cpu(shdr64.sh_offset) + file64_to_cpu(shdr64.sh_size);
+            return sht_end > last_section_end ? sht_end : last_section_end;
+        }
+
+        ssize_t ElfFile::getSize() {
+            off_t ret;
+            FILE* fd = NULL;
+            off_t size = -1;
+
+            fd = fopen(fname, "rb");
+            if (fd == NULL) {
+                Logger::error(std::string("Cannot open ") + fname + ": " + strerror(errno));
+                return -1;
+            }
+            ret = fread(ehdr.e_ident, 1, EI_NIDENT, fd);
+            if (ret != EI_NIDENT) {
+                Logger::error(std::string("Read of e_ident from ") + fname
+                                             + " failed: " + strerror(errno));
+                return -1;
+            }
+            if ((ehdr.e_ident[EI_DATA] != ELFDATA2LSB) &&
+                (ehdr.e_ident[EI_DATA] != ELFDATA2MSB)) {
+                Logger::error("Unknown ELF data order " + std::to_string(ehdr.e_ident[EI_DATA]));
+                return -1;
+            }
+            if (ehdr.e_ident[EI_CLASS] == ELFCLASS32) {
+                size = read_elf32(fd);
+            } else if (ehdr.e_ident[EI_CLASS] == ELFCLASS64) {
+                size = read_elf64(fd);
+            } else {
+                Logger::error("Unknown ELF class: " + std::to_string(ehdr.e_ident[EI_CLASS]));
+                return -1;
+            }
+
+            fclose(fd);
+            return size;
+        }
+    }
+}
diff --git a/src/libappimage/utils/ElfFile.h b/src/libappimage/utils/ElfFile.h
new file mode 100644 (file)
index 0000000..a9bd0da
--- /dev/null
@@ -0,0 +1,54 @@
+#pragma once
+
+// system
+#include <string>
+#include <cstring>
+
+// local
+#include "light_elf.h"
+#include "Logger.h"
+
+namespace appimage {
+    namespace utils {
+        /**
+         * Utility class to read elf files. Not meant to be feature complete
+         */
+        class ElfFile {
+        public:
+            explicit ElfFile(const std::string& path);
+
+
+            /*
+             * Calculate the size of an ELF file on disk based on the information in its header
+             *
+             * Example:
+             *
+             * ls -l   126584
+             *
+             * Calculation using the values also reported by readelf -h:
+             * Start of section headers        e_shoff         124728
+             * Size of section headers         e_shentsize     64
+             * Number of section headers       e_shnum         29
+             *
+             * e_shoff + ( e_shentsize * e_shnum ) =   126584
+             */
+            ssize_t getSize();
+
+        private:
+            std::string path;
+            const char* fname;
+            Elf64_Ehdr ehdr;
+
+            uint16_t file16_to_cpu(uint16_t val);
+
+            uint32_t file32_to_cpu(uint32_t val);
+
+            uint64_t file64_to_cpu(uint64_t val);
+
+            off_t read_elf32(FILE* fd);
+
+            off_t read_elf64(FILE* fd);
+
+        };
+    }
+}
diff --git a/src/libappimage/utils/IconHandle.cpp b/src/libappimage/utils/IconHandle.cpp
new file mode 100644 (file)
index 0000000..75c3feb
--- /dev/null
@@ -0,0 +1,54 @@
+// system
+#include <sstream>
+#include <iostream>
+#include <fstream>
+
+// libraries
+#include <boost/filesystem.hpp>
+#include <boost/iostreams/filtering_stream.hpp>
+#include <boost/iostreams/device/back_inserter.hpp>
+
+
+// local
+#include "DLHandle.h"
+#include "IconHandle.h"
+
+namespace bf = boost::filesystem;
+namespace io = boost::iostreams;
+
+#include "IconHandleCairoRsvg.h"
+
+class appimage::utils::IconHandle::Priv : public IconHandleCairoRsvg {
+public:
+    Priv(const std::vector<char>& data) : IconHandleCairoRsvg(data) {}
+
+    Priv(const std::string& path) : IconHandleCairoRsvg(path) {}
+};
+
+namespace appimage {
+    namespace utils {
+
+        IconHandle::IconHandle(std::vector<char>& data) : d(new Priv(data)) {}
+
+        int IconHandle::getSize() { return d->getSize(); }
+
+        std::string IconHandle::format() { return d->getFormat(); }
+
+        void IconHandle::setSize(int size) { d->setSize(size); }
+
+        void IconHandle::save(const std::string& path, const std::string& format) {
+            bf::path bPath(path);
+            try { bf::create_directories(bPath.parent_path()); }
+            catch (const bf::filesystem_error&) { throw IconHandleError("Unable to create parent path"); }
+
+            d->save(bPath, format);
+        }
+
+        IconHandle::IconHandle(const std::string& path) : d(new Priv(path)) {}
+
+        IconHandle::~IconHandle() = default;
+
+        IconHandleError::IconHandleError(const std::string& what) : runtime_error(what) {}
+    }
+}
+
diff --git a/src/libappimage/utils/IconHandle.h b/src/libappimage/utils/IconHandle.h
new file mode 100644 (file)
index 0000000..1fdf9b4
--- /dev/null
@@ -0,0 +1,73 @@
+#pragma once
+
+// system
+#include <vector>
+#include <memory>
+
+namespace appimage {
+    namespace utils {
+
+        /**
+         * Provide the image manipulation functions required by libappimage, nothing more.
+         * Currently are supported two image formats: png and svg. Those formats are the
+         * ones recommended for creating icons at the FreeDesktop Icon Theme Specification.
+         * See: https://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html
+         *
+         * This implementation uses libcairo and librsvg as backend. Those libraries are
+         * dynamically loaded at runtime so they are not required for building (or linking) the
+         * binaries.
+         */
+        class IconHandle {
+        public:
+            /**
+             * Create a IconHandle instance from <data>
+             * @param data
+             * @throw IconHandleError in case of a backend error or an unsupported image format
+             */
+            explicit IconHandle(std::vector<char>& data);
+
+            /**
+             * Create an IconHandle from a the file pointed by <path>
+             * @param path
+             */
+            explicit IconHandle(const std::string &path);
+
+            /**
+             * @brief Save the icon to <path> with <format>.
+             *
+             * @param path target path
+             * @throw IconHandleError in case of error
+             */
+            void save(const std::string& path, const std::string& format = "png");
+
+            /**
+             * @return the icon size
+             */
+            int getSize();
+
+            /**
+             * @brief Set a new size to the Icon.
+             * @param size
+             */
+            void setSize(int size);
+
+            /**
+             * @return the image format ("png" or "svg")
+             */
+            std::string format();
+
+            virtual ~IconHandle();
+
+        private:
+            class Priv;
+
+            std::unique_ptr<Priv> d;
+        };
+
+        class IconHandleError : public std::runtime_error {
+        public:
+            explicit IconHandleError(const std::string& what);
+        };
+    }
+
+}
diff --git a/src/libappimage/utils/IconHandleCairoRsvg.cpp b/src/libappimage/utils/IconHandleCairoRsvg.cpp
new file mode 100644 (file)
index 0000000..63aab43
--- /dev/null
@@ -0,0 +1,236 @@
+// libraries
+#include <glib-object.h>
+#include <fstream>
+
+// local
+#include "IconHandle.h"
+#include "IconHandleCairoRsvg.h"
+
+namespace appimage {
+    namespace utils {
+
+        struct ReadCtx {
+            ReadCtx(const uint8_t* data, unsigned int left) : data(data), left(left) {}
+
+            const uint8_t* data;
+            unsigned left;
+        };
+
+        static _cairo_status cairoReadFunc(void* closure, unsigned char* data, unsigned int length) {
+            auto readCtx = static_cast<struct ReadCtx*>(closure);
+
+            if (!readCtx->left)
+                return CAIRO_STATUS_READ_ERROR;
+
+            if (length > readCtx->left)
+                length = readCtx->left;
+
+            memcpy(data, readCtx->data, length);
+            readCtx->data += length;
+            readCtx->left -= length;
+
+            return CAIRO_STATUS_SUCCESS;
+        }
+
+        static _cairo_status cairoWriteFunc(void* closure,
+                                            const unsigned char* data,
+                                            unsigned int length) {
+            // cast back the vector passed as user parameter on cairo_surface_write_to_png_stream
+            // see the cairo_surface_write_to_png_stream doc for details
+            auto outData = static_cast<std::vector<uint8_t>*>(closure);
+
+            auto offset = static_cast<unsigned int>(outData->size());
+            outData->resize(offset + length);
+
+            memcpy(outData->data() + offset, data, length);
+
+            return CAIRO_STATUS_SUCCESS;
+        }
+
+        IconHandleCairoRsvg::IconHandleCairoRsvg(const std::vector<char>& data) : IconHandlePriv(data) {
+            // make sure that the data is placed in a contiguous block
+            originalData.resize(data.size());
+            std::move(data.begin(), data.end(), originalData.begin());
+
+            // guess the image format by trying to load it
+            if (!tryLoadPng(originalData) && !tryLoadSvg(originalData)) {
+                throw IconHandleError("Unable to load image.");
+            }
+
+            iconSize = iconOriginalSize = getOriginalSize();
+        }
+
+        IconHandleCairoRsvg::IconHandleCairoRsvg(const std::string& path) : IconHandlePriv(path) {
+            readFile(path);
+
+            // guess the image format by trying to load it
+            if (!tryLoadPng(originalData) && !tryLoadSvg(originalData))
+                throw IconHandleError("Unable to load image.");
+
+            iconSize = iconOriginalSize = getOriginalSize();
+        }
+
+        IconHandleCairoRsvg::~IconHandleCairoRsvg() {
+            if (cairoSurface != nullptr)
+                cairo_surface_destroy(cairoSurface);
+
+            if (rsvgHandle != nullptr)
+                g_object_unref(rsvgHandle);
+        }
+
+        int IconHandleCairoRsvg::getOriginalSize() {
+            // Icons are squared so we only have to query for one size value
+            if (imageFormat == "png" && cairoSurface != nullptr)
+                return cairo_image_surface_get_height(cairoSurface);
+
+
+            if (imageFormat == "svg" && rsvgHandle != nullptr) {
+                RsvgDimensionData dimensions = {};
+                rsvg_handle_get_dimensions(rsvgHandle, &dimensions);
+
+                return dimensions.height;
+            }
+
+            throw IconHandleError("Malformed IconHandle");
+        }
+
+        int IconHandleCairoRsvg::getSize() const { return iconSize; }
+
+        void IconHandleCairoRsvg::setSize(int newSize) { IconHandleCairoRsvg::iconSize = newSize; }
+
+        const std::string& IconHandleCairoRsvg::getFormat() const { return imageFormat; }
+
+        void IconHandleCairoRsvg::save(const boost::filesystem::path& path, const std::string& targetFormat) {
+            const auto& output = getNewIconData(targetFormat);
+
+            if (output.empty())
+                throw IconHandleError("Unable to transform " + imageFormat + " into " + targetFormat);
+
+            std::ofstream ofstream(path.string(), std::ios::out | std::ios::binary | std::ios::trunc);
+            if (ofstream.is_open())
+                ofstream.write(reinterpret_cast<const char*>(output.data()), output.size());
+            else
+                throw IconHandleError("Unable to write into: " + path.string());
+        }
+
+        bool IconHandleCairoRsvg::tryLoadSvg(const std::vector<char>& data) {
+            rsvgHandle = rsvg_handle_new_from_data(reinterpret_cast<const uint8_t*>(data.data()), data.size(),
+                                                   nullptr);
+
+            if (rsvgHandle) {
+                imageFormat = "svg";
+                return true;
+            } else
+                return false;
+        }
+
+        bool IconHandleCairoRsvg::tryLoadPng(const std::vector<char>& data) {
+            ReadCtx readCtx(reinterpret_cast<const uint8_t*>(data.data()), data.size());
+            cairoSurface = cairo_image_surface_create_from_png_stream(cairoReadFunc, &readCtx);
+
+            auto status = cairo_surface_status(cairoSurface);
+            if (status == 0) {
+                // All went ok, we have a PNG image
+                imageFormat = "png";
+                return true;
+            } else {
+                // It's not a PNG, let's clean up that surface
+                cairo_surface_destroy(cairoSurface);
+                cairoSurface = nullptr;
+                return false;
+            }
+        }
+
+        std::vector<char> IconHandleCairoRsvg::svg2png() {
+            // prepare cairo rendering surface
+            cairo_surface_t* surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, iconSize, iconSize);
+            cairo_t* cr = cairo_create(surface);
+
+
+            if (iconOriginalSize != iconSize && iconOriginalSize != 0) {
+                // Scale Image
+                double scale_factor = iconSize / iconOriginalSize;
+
+                // set scale factor
+                cairo_scale(cr, scale_factor, scale_factor);
+            }
+
+            // render
+            rsvg_handle_render_cairo(rsvgHandle, cr);
+
+            std::vector<char> out;
+            cairo_surface_write_to_png_stream(surface, cairoWriteFunc, &out);
+
+            // clean
+            cairo_destroy(cr);
+            cairo_surface_destroy(surface);
+
+            return out;
+        }
+
+        std::vector<char> IconHandleCairoRsvg::png2png() {
+            // no transformation required
+            if (iconOriginalSize == iconSize)
+                return originalData;
+            else {
+                // load original image
+                ReadCtx readCtx(reinterpret_cast<const uint8_t*>(originalData.data()), originalData.size());
+                cairo_surface_t* sourceSurface = cairo_image_surface_create_from_png_stream(cairoReadFunc, &readCtx);
+
+                // prepare cairo rendering surface
+                cairo_surface_t* targetSurface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, iconSize, iconSize);
+                cairo_t* cr = cairo_create(targetSurface);
+
+                if (iconOriginalSize != iconSize && iconOriginalSize != 0) {
+                    // Scale Image
+                    double scale_factor = iconSize / iconOriginalSize;
+
+                    cairo_scale(cr, scale_factor, scale_factor);
+                    cairo_set_source_surface(cr, sourceSurface, 0, 0);
+                    cairo_paint(cr);
+                }
+
+
+                std::vector<char> out;
+                cairo_surface_write_to_png_stream(targetSurface, cairoWriteFunc, &out);
+
+                // clean
+                cairo_destroy(cr);
+                cairo_surface_destroy(sourceSurface);
+                cairo_surface_destroy(targetSurface);
+
+                return out;
+            }
+        }
+
+        void IconHandleCairoRsvg::readFile(const std::string& path) {
+            std::ifstream in(path, std::ios_base::in | std::ios_base::binary | std::ios_base::ate);
+
+            auto size = static_cast<unsigned long>(in.tellg());
+            originalData.resize(size);
+
+            in.seekg(0, std::ios_base::beg);
+            in.read(reinterpret_cast<char*>(originalData.data()), size);
+        }
+
+        std::vector<char> IconHandleCairoRsvg::getNewIconData(const std::string& targetFormat) {
+            if (targetFormat == "png") {
+                if (imageFormat == "svg")
+                    return svg2png();
+
+                if (imageFormat == "png")
+                    return png2png();
+            }
+
+            if (targetFormat == "svg") {
+                if (imageFormat == "svg")
+                    return originalData; // svgs doens't require to be resized
+
+                if (imageFormat == "png")
+                    throw IconHandleError("png to svg conversion is not supported");
+            }
+
+            throw IconHandleError("Unsuported format");
+        }
+    }
+}
diff --git a/src/libappimage/utils/IconHandleCairoRsvg.h b/src/libappimage/utils/IconHandleCairoRsvg.h
new file mode 100644 (file)
index 0000000..b23c77e
--- /dev/null
@@ -0,0 +1,68 @@
+#pragma once
+// libraries
+extern "C" {
+#include <cairo-svg.h>
+#include <librsvg/rsvg.h>
+
+// Workaround warning "Including <librsvg/rsvg-cairo.h> directly is deprecated."
+#if !defined(RSVG_CAIRO_H)
+#include <librsvg/rsvg-cairo.h>
+#endif
+}
+
+// local
+#include "IconHandlePriv.h"
+
+namespace appimage {
+    namespace utils {
+
+        class IconHandleCairoRsvg : public IconHandlePriv {
+        public:
+            explicit IconHandleCairoRsvg(const std::vector<char>& data);
+
+            explicit IconHandleCairoRsvg(const std::string& path);
+
+            ~IconHandleCairoRsvg() override;
+
+            int getOriginalSize() override;
+
+            int getSize() const override;
+
+            void setSize(int newSize) override;
+
+            const std::string& getFormat() const override;
+
+            void save(const boost::filesystem::path& path, const std::string& targetFormat) override;
+
+        private:
+            std::vector<char> originalData;
+
+            int iconSize;
+            int iconOriginalSize;
+            std::string imageFormat;
+
+            RsvgHandle* rsvgHandle = nullptr;
+            cairo_surface_t* cairoSurface = nullptr;
+
+            bool tryLoadSvg(const std::vector<char>& data);
+
+            bool tryLoadPng(const std::vector<char>& data);
+
+            /**
+             * Render the svg as an image of size <iconSize>
+             * @return raw image data
+             */
+            std::vector<char> svg2png();
+
+            /**
+             * Resize the original image if required
+             * @return raw image data
+             */
+            std::vector<char> png2png();
+
+            void readFile(const std::string& path);
+
+            std::vector<char> getNewIconData(const std::string& targetFormat);
+        };
+    }
+}
diff --git a/src/libappimage/utils/IconHandleDLOpenCairoRsvg.cpp b/src/libappimage/utils/IconHandleDLOpenCairoRsvg.cpp
new file mode 100644 (file)
index 0000000..14307ed
--- /dev/null
@@ -0,0 +1,164 @@
+#include "IconHandleDLOpenCairoRsvg.h"
+
+namespace appimage {
+    namespace utils {
+        IconHandleDLOpenCairoRsvg::IconHandleDLOpenCairoRsvg(const std::vector<char>& data) : IconHandlePriv(data) {
+            // make sure that the data is placed in a contiguous block
+            originalData.resize(data.size());
+            std::move(data.begin(), data.end(), originalData.begin());
+
+            // guess the image format by trying to load it
+            if (!tryLoadPng(originalData) && !tryLoadSvg(originalData)) {
+                throw IconHandleError("Unable to load image.");
+            }
+
+            iconSize = iconOriginalSize = getOriginalSize();
+        }
+
+        IconHandleDLOpenCairoRsvg::IconHandleDLOpenCairoRsvg(const std::string& path) : IconHandlePriv(path) {
+            readFile(path);
+
+            // guess the image format by trying to load it
+            if (!tryLoadPng(originalData) && !tryLoadSvg(originalData))
+                throw IconHandleError("Unable to load image.");
+
+            iconSize = iconOriginalSize = getOriginalSize();
+        }
+
+        IconHandleDLOpenCairoRsvg::~IconHandleDLOpenCairoRsvg() {
+            if (cairoSurface != nullptr)
+                cairo.surface_destroy(cairoSurface);
+
+            if (rsvgHandle != nullptr)
+                glibOjbect.object_unref(rsvgHandle);
+        }
+
+        int IconHandleDLOpenCairoRsvg::getOriginalSize() {
+            // Icons are squared so we only have to query for one size value
+            if (imageFormat == "png" && cairoSurface != nullptr)
+                return cairo.image_surface_get_height(cairoSurface);
+
+
+            if (imageFormat == "svg" && rsvgHandle != nullptr) {
+                IconHandleDLOpenCairoRsvg::RSvgHandle::RSvgDimensionData dimensions = {};
+                rsvg.handle_get_dimensions(rsvgHandle, &dimensions);
+
+                return dimensions.height;
+            }
+
+            throw IconHandleError("Malformed IconHandle");
+        }
+
+        int IconHandleDLOpenCairoRsvg::getSize() const { return iconSize; }
+
+        void IconHandleDLOpenCairoRsvg::setSize(int newSize) { IconHandleDLOpenCairoRsvg::iconSize = newSize; }
+
+        const std::string& IconHandleDLOpenCairoRsvg::getFormat() const { return imageFormat; }
+
+        void IconHandleDLOpenCairoRsvg::save(const boost::filesystem::path& path, const std::string& targetFormat) {
+            const auto& output = getNewIconData(targetFormat);
+
+            if (output.empty())
+                throw IconHandleError("Unable to transform " + imageFormat + " into " + targetFormat);
+
+            std::ofstream ofstream(path.string(), std::ios::out | std::ios::binary | std::ios::trunc);
+            if (ofstream.is_open())
+                ofstream.write(reinterpret_cast<const char*>(output.data()), output.size());
+            else
+                throw IconHandleError("Unable to write into: " + path.string());
+        }
+
+        bool IconHandleDLOpenCairoRsvg::tryLoadSvg(const std::vector<char>& data) {
+            rsvgHandle = rsvg.handle_new_from_data(reinterpret_cast<const uint8_t*>(data.data()), data.size(),
+                                                   nullptr);
+
+            if (rsvgHandle) {
+                imageFormat = "svg";
+                return true;
+            } else
+                return false;
+        }
+
+        bool IconHandleDLOpenCairoRsvg::tryLoadPng(const std::vector<char>& data) {
+            CairoHandle::ReadCtx readCtx(reinterpret_cast<const uint8_t*>(data.data()), data.size());
+            cairoSurface = cairo.image_surface_create_from_png_stream(CairoHandle::cairoReadFunc, &readCtx);
+
+            auto status = cairo.surface_status(cairoSurface);
+            if (status == 0) {
+                // All went ok, we have a PNG image
+                imageFormat = "png";
+                return true;
+            } else {
+                // It's not a PNG, let's clean up that surface
+                cairo.surface_destroy(cairoSurface);
+                cairoSurface = nullptr;
+                return false;
+            }
+        }
+
+        std::vector<char> IconHandleDLOpenCairoRsvg::svg2png() {
+            // prepare cairo rendering surface
+            void* surface = cairo.image_surface_create(0, iconSize, iconSize);
+            void* cr = cairo.create(surface);
+
+
+            if (iconOriginalSize != iconSize && iconOriginalSize != 0) {
+                // Scale Image
+                double scale_factor = iconSize / iconOriginalSize;
+
+                // set scale factor
+                cairo.scale(cr, scale_factor, scale_factor);
+            }
+
+            // render
+            rsvg.handle_render_cairo(rsvgHandle, cr);
+
+            std::vector<char> out;
+            cairo.surface_write_to_png_stream(surface, CairoHandle::cairoWriteFunc, &out);
+
+            // clean
+            cairo.destroy(cr);
+            cairo.surface_destroy(surface);
+
+            return out;
+        }
+
+        std::vector<char> IconHandleDLOpenCairoRsvg::png2png() {
+            // no transformation required
+            if (iconOriginalSize == iconSize)
+                return originalData;
+            else
+                throw IconHandleError("png resizing is not supported");
+        }
+
+        void IconHandleDLOpenCairoRsvg::readFile(const std::string& path) {
+            std::ifstream in(path, std::ios_base::in | std::ios_base::binary | std::ios_base::ate);
+
+            auto size = static_cast<unsigned long>(in.tellg());
+            originalData.resize(size);
+
+            in.seekg(0, std::ios_base::beg);
+            in.read(reinterpret_cast<char*>(originalData.data()), size);
+        }
+
+        std::vector<char> IconHandleDLOpenCairoRsvg::getNewIconData(const std::string& targetFormat) {
+            if (targetFormat == "png") {
+                if (imageFormat == "svg")
+                    return svg2png();
+
+                if (imageFormat == "png")
+                    return png2png();
+            }
+
+            if (targetFormat == "svg") {
+                if (imageFormat == "svg")
+                    return originalData; // svgs doens't require to be resized
+
+                if (imageFormat == "png")
+                    throw IconHandleError("png to svg conversion is not supported");
+            }
+
+            throw IconHandleError("Unsuported format");
+        }
+    }
+}
diff --git a/src/libappimage/utils/IconHandleDLOpenCairoRsvg.h b/src/libappimage/utils/IconHandleDLOpenCairoRsvg.h
new file mode 100644 (file)
index 0000000..476bb6e
--- /dev/null
@@ -0,0 +1,293 @@
+#pragma once
+// system
+#include <string>
+#include <vector>
+#include <cstring>
+#include <fstream>
+
+// libraries
+#include <boost/filesystem/path.hpp>
+
+// local
+#include "DLHandle.h"
+#include "IconHandlePriv.h"
+#include "IconHandle.h"
+
+namespace appimage {
+    namespace utils {
+        class IconHandleDLOpenCairoRsvg : public IconHandlePriv {
+
+        public:
+            explicit IconHandleDLOpenCairoRsvg(const std::vector<char>& data);
+
+            explicit IconHandleDLOpenCairoRsvg(const std::string& path);
+
+            ~IconHandleDLOpenCairoRsvg() override;
+
+            int getOriginalSize() override;
+
+            int getSize() const override;
+
+            void setSize(int newSize) override;
+
+            const std::string& getFormat() const override;
+
+            void save(const boost::filesystem::path& path, const std::string& targetFormat) override;
+
+        private:
+            struct RSvgHandle : protected DLHandle {
+                // rsvg API symbols
+                struct RSvgDimensionData {
+                    int width;
+                    int height;
+                    double em;
+                    double ex;
+                };
+
+                typedef void* (* rsvg_handle_new_from_data_t)(const uint8_t* data, unsigned long data_len,
+                                                              void** error);
+
+                typedef void* (* rsvg_handle_new_from_file_t )(const char* file_name, void** error);
+
+                typedef bool (* rsvg_handle_render_cairo_t)(void* handle, void* cr);
+
+                typedef void(* rsvg_handle_get_dimensions_t)(void* handle, RSvgDimensionData* dimension_data);
+
+                /**
+                 * @brief Load librsvg-2 and resolve the symbol addresses required by the IconHandle.
+                 *
+                 * Mode comments:
+                 * RTLD_LAZY - load the lib only the required symbols
+                 * RTLD_NODELETE - do not unload the lib, as it wasn't designed to be used this way it
+                 *                  will produce a big crash.
+                 */
+                RSvgHandle() : DLHandle("librsvg-2.so.2", RTLD_LAZY | RTLD_NODELETE) {
+                    DLHandle::loadSymbol(handle_new_from_data, "rsvg_handle_new_from_data");
+                    DLHandle::loadSymbol(handle_render_cairo, "rsvg_handle_render_cairo");
+                    DLHandle::loadSymbol(handle_get_dimensions, "rsvg_handle_get_dimensions");
+                    DLHandle::loadSymbol(handle_new_from_file, "rsvg_handle_new_from_file");
+                }
+
+                rsvg_handle_new_from_data_t handle_new_from_data = nullptr;
+                rsvg_handle_render_cairo_t handle_render_cairo = nullptr;
+                rsvg_handle_get_dimensions_t handle_get_dimensions = nullptr;
+                rsvg_handle_new_from_file_t handle_new_from_file = nullptr;
+            };
+
+            class CairoHandle : protected DLHandle {
+                // cairo API symbols
+                typedef enum _cairo_status {
+                    CAIRO_STATUS_SUCCESS = 0,
+
+                    CAIRO_STATUS_NO_MEMORY,
+                    CAIRO_STATUS_INVALID_RESTORE,
+                    CAIRO_STATUS_INVALID_POP_GROUP,
+                    CAIRO_STATUS_NO_CURRENT_POINT,
+                    CAIRO_STATUS_INVALID_MATRIX,
+                    CAIRO_STATUS_INVALID_STATUS,
+                    CAIRO_STATUS_NULL_POINTER,
+                    CAIRO_STATUS_INVALID_STRING,
+                    CAIRO_STATUS_INVALID_PATH_DATA,
+                    CAIRO_STATUS_READ_ERROR,
+                    CAIRO_STATUS_WRITE_ERROR,
+                    CAIRO_STATUS_SURFACE_FINISHED,
+                    CAIRO_STATUS_SURFACE_TYPE_MISMATCH,
+                    CAIRO_STATUS_PATTERN_TYPE_MISMATCH,
+                    CAIRO_STATUS_INVALID_CONTENT,
+                    CAIRO_STATUS_INVALID_FORMAT,
+                    CAIRO_STATUS_INVALID_VISUAL,
+                    CAIRO_STATUS_FILE_NOT_FOUND,
+                    CAIRO_STATUS_INVALID_DASH,
+                    CAIRO_STATUS_INVALID_DSC_COMMENT,
+                    CAIRO_STATUS_INVALID_INDEX,
+                    CAIRO_STATUS_CLIP_NOT_REPRESENTABLE,
+                    CAIRO_STATUS_TEMP_FILE_ERROR,
+                    CAIRO_STATUS_INVALID_STRIDE,
+                    CAIRO_STATUS_FONT_TYPE_MISMATCH,
+                    CAIRO_STATUS_USER_FONT_IMMUTABLE,
+                    CAIRO_STATUS_USER_FONT_ERROR,
+                    CAIRO_STATUS_NEGATIVE_COUNT,
+                    CAIRO_STATUS_INVALID_CLUSTERS,
+                    CAIRO_STATUS_INVALID_SLANT,
+                    CAIRO_STATUS_INVALID_WEIGHT,
+                    CAIRO_STATUS_INVALID_SIZE,
+                    CAIRO_STATUS_USER_FONT_NOT_IMPLEMENTED,
+                    CAIRO_STATUS_DEVICE_TYPE_MISMATCH,
+                    CAIRO_STATUS_DEVICE_ERROR,
+                    CAIRO_STATUS_INVALID_MESH_CONSTRUCTION,
+                    CAIRO_STATUS_DEVICE_FINISHED,
+                    CAIRO_STATUS_JBIG2_GLOBAL_MISSING,
+                    CAIRO_STATUS_PNG_ERROR,
+                    CAIRO_STATUS_FREETYPE_ERROR,
+                    CAIRO_STATUS_WIN32_GDI_ERROR,
+                    CAIRO_STATUS_TAG_ERROR,
+
+                    CAIRO_STATUS_LAST_STATUS
+                } cairo_status_t;
+
+                typedef void* (* cairo_image_surface_create_t)(int format, int width, int height);
+
+                typedef void* (* cairo_create_t)(void* target);
+
+                typedef int (* cairo_write_func_t)(void* closure, const unsigned char* data,
+                                                   unsigned int length);
+
+                typedef int (* cairo_surface_write_to_png_stream_t)(void* surface, cairo_write_func_t write_func,
+                                                                    void* closure);
+
+                typedef void (* cairo_destroy_t)(void* cr);
+
+                typedef void (* cairo_surface_destroy_t)(void* surface);
+
+                typedef void (* cairo_scale_t)(void* cr, double sx, double sy);
+
+                typedef cairo_status_t (* cairo_read_func_t)(void* closure,
+                                                             unsigned char* data,
+                                                             unsigned int length);
+
+                typedef void* (* cairo_image_surface_create_from_png_stream_t)(cairo_read_func_t read_func,
+                                                                               void* closure);
+
+                typedef void* (* cairo_image_surface_create_from_png_t )(const char* filename);
+
+                typedef int (* cairo_surface_status_t)(void* surface);
+
+                typedef int (* cairo_image_surface_get_height_t)(void* surface);
+
+                typedef const char* (* cairo_status_to_string_t)(int status);
+
+
+            public:
+                /**
+                 * @brief Load libcairo.so.2 and resolve the symbol addresses required by the IconHandle.
+                 *
+                 * Mode comments:
+                 * RTLD_LAZY - load the lib only the required symbols
+                 * RTLD_NODELETE - do not unload the lib, as it wasn't designed to be used this way it
+                 *                  will produce a big crash.
+                 */
+                CairoHandle() : DLHandle("libcairo.so.2", RTLD_LAZY | RTLD_NODELETE) {
+                    DLHandle::loadSymbol(image_surface_create, "cairo_image_surface_create");
+                    DLHandle::loadSymbol(create, "cairo_create");
+                    DLHandle::loadSymbol(surface_write_to_png_stream, "cairo_surface_write_to_png_stream");
+                    DLHandle::loadSymbol(destroy, "cairo_destroy");
+                    DLHandle::loadSymbol(surface_destroy, "cairo_surface_destroy");
+                    DLHandle::loadSymbol(scale, "cairo_scale");
+                    DLHandle::loadSymbol(surface_status, "cairo_surface_status");
+                    DLHandle::loadSymbol(image_surface_create_from_png_stream,
+                                         "cairo_image_surface_create_from_png_stream");
+                    DLHandle::loadSymbol(image_surface_create_from_png, "cairo_image_surface_create_from_png");
+                    DLHandle::loadSymbol(image_surface_get_height, "cairo_image_surface_get_height");
+                    DLHandle::loadSymbol(status_to_string, "cairo_status_to_string");
+                }
+
+
+                static int cairoWriteFunc(void* closure, const unsigned char* data, unsigned int length) {
+                    // cast back the vector passed as user parameter on cairo_surface_write_to_png_stream
+                    // see the cairo_surface_write_to_png_stream doc for details
+                    auto outData = static_cast<std::vector<uint8_t>*>(closure);
+
+                    auto offset = static_cast<unsigned int>(outData->size());
+                    outData->resize(offset + length);
+
+                    memcpy(outData->data() + offset, data, length);
+
+                    return CAIRO_STATUS_SUCCESS;
+                }
+
+                struct ReadCtx {
+                    ReadCtx(const uint8_t* data, unsigned int left) : data(data), left(left) {}
+
+                    const uint8_t* data;
+                    unsigned left;
+                };
+
+                static _cairo_status cairoReadFunc(void* closure, unsigned char* data, unsigned int length) {
+                    auto readCtx = static_cast<struct ReadCtx*>(closure);
+
+                    if (!readCtx->left)
+                        return CAIRO_STATUS_READ_ERROR;
+
+                    if (length > readCtx->left)
+                        length = readCtx->left;
+
+                    memcpy(data, readCtx->data, length);
+                    readCtx->data += length;
+                    readCtx->left -= length;
+
+                    return CAIRO_STATUS_SUCCESS;
+                }
+
+                cairo_image_surface_create_t image_surface_create = nullptr;
+                cairo_create_t create = nullptr;
+                cairo_surface_write_to_png_stream_t surface_write_to_png_stream = nullptr;
+                cairo_destroy_t destroy = nullptr;
+                cairo_surface_destroy_t surface_destroy = nullptr;
+                cairo_scale_t scale = nullptr;
+                cairo_image_surface_create_from_png_stream_t image_surface_create_from_png_stream = nullptr;
+                cairo_image_surface_create_from_png_t image_surface_create_from_png = nullptr;
+                cairo_surface_status_t surface_status = nullptr;
+                cairo_image_surface_get_height_t image_surface_get_height = nullptr;
+                cairo_status_to_string_t status_to_string = nullptr;
+            };
+
+            struct GLibOjbectHandle : protected DLHandle {
+                // GlibOjbect API symbols
+                typedef void (* g_object_unref_t)(void* object);
+
+                g_object_unref_t object_unref = nullptr;
+
+                /**
+                 * @brief Load libgobject-2 and resolve the symbol addresses required by the IconHandle.
+                 *
+                 * Known library name by distribution:
+                 * - CentOS: libgobject-2.0.so.0
+                 * - Debian/Ubuntu: libgobject-2.0.so
+                 *
+                 * Mode comments:
+                 * RTLD_LAZY - load the lib only the required symbols
+                 * RTLD_NODELETE - do not unload the lib, as it wasn't designed to be used this way it
+                 *                  will produce a big crash.
+                 */
+                GLibOjbectHandle() : DLHandle({"libgobject-2.0.so", "libgobject-2.0.so.0"}, RTLD_LAZY | RTLD_NODELETE) {
+                    DLHandle::loadSymbol(object_unref, "g_object_unref");
+                }
+            };
+
+            RSvgHandle rsvg;
+            CairoHandle cairo;
+            GLibOjbectHandle glibOjbect;
+
+
+            std::vector<char> originalData;
+
+            int iconSize;
+            int iconOriginalSize;
+            std::string imageFormat;
+
+            void* rsvgHandle = nullptr;
+            void* cairoSurface = nullptr;
+
+            bool tryLoadSvg(const std::vector<char>& data);
+
+            bool tryLoadPng(const std::vector<char>& data);
+
+            /**
+             * Render the svg as an image of size <iconSize>
+             * @return raw image data
+             */
+            std::vector<char> svg2png();
+
+            /**
+             * Resize the original image if required
+             * @return raw image data
+             */
+            std::vector<char> png2png();
+
+            void readFile(const std::string& path);
+
+            std::vector<char> getNewIconData(const std::string& targetFormat);
+        };
+    }
+}
+
diff --git a/src/libappimage/utils/IconHandlePriv.h b/src/libappimage/utils/IconHandlePriv.h
new file mode 100644 (file)
index 0000000..3bf40a3
--- /dev/null
@@ -0,0 +1,34 @@
+#pragma once
+// system
+#include <vector>
+#include <string>
+
+// libraries
+#include <boost/filesystem.hpp>
+
+namespace appimage {
+    namespace utils {
+
+/**
+ * Private interface of the icon handler
+ */
+        class IconHandlePriv {
+        public:
+            explicit IconHandlePriv(const std::vector<char>& data) {}
+
+            explicit IconHandlePriv(const std::string& path) {}
+
+            virtual ~IconHandlePriv() = default;
+
+            virtual int getOriginalSize() = 0;
+
+            virtual int getSize() const = 0;
+
+            virtual void setSize(int iconSize) = 0;
+
+            virtual const std::string& getFormat() const = 0;
+
+            virtual void save(const boost::filesystem::path& path, const std::string& targetFormat) = 0;
+        };
+    }
+}
diff --git a/src/libappimage/utils/Logger.cpp b/src/libappimage/utils/Logger.cpp
new file mode 100644 (file)
index 0000000..35748bc
--- /dev/null
@@ -0,0 +1,86 @@
+// system
+#include <iostream>
+
+// libraries
+#include <boost/filesystem.hpp>
+
+// local
+#include "Logger.h"
+
+
+namespace appimage {
+    namespace utils {
+        class Logger::Priv {
+        public:
+            // singleton
+            static std::unique_ptr<Logger> i;
+
+            Priv() {
+                // Default logging function
+                logFunction = [](LogLevel level, const std::string& message) {
+                    switch (level) {
+                        case LogLevel::INFO:
+                            std::clog << "INFO: ";
+                            break;
+                        case LogLevel::DEBUG:
+                            std::clog << "DEBUG: ";
+                            break;
+                        case LogLevel::WARNING:
+                            std::clog << "WARNING: ";
+                            break;
+                        case LogLevel::ERROR:
+                            std::clog << "ERROR: ";
+                            break;
+                    }
+
+                    std::clog << message << std::endl;
+                };
+            }
+
+            log_callback_t logFunction;
+        };
+
+        std::unique_ptr<Logger> Logger::Priv::i = nullptr;
+
+        Logger::Logger() : d(new Priv) {}
+
+        Logger* Logger::getInstance() {
+            if (!Priv::i)
+                Priv::i.reset(new Logger());
+
+            return Priv::i.get();
+        }
+
+        void Logger::setCallback(const log_callback_t& callback) {
+            d->logFunction = callback;
+        }
+
+        void Logger::log(const utils::LogLevel& level, const std::string& message) {
+            d->logFunction(level, message);
+        }
+
+        void Logger::debug(const std::string& message) {
+            const auto i = getInstance();
+            i->log(LogLevel::DEBUG, message);
+        }
+
+        void Logger::info(const std::string& message) {
+            const auto i = getInstance();
+            i->log(LogLevel::INFO, message);
+        }
+
+        void Logger::warning(const std::string& message) {
+            const auto i = getInstance();
+            i->log(LogLevel::WARNING, message);
+        }
+
+        void Logger::error(const std::string& message) {
+            const auto i = getInstance();
+            i->log(LogLevel::ERROR, message);
+        }
+
+        void setLoggerCallback(const log_callback_t& callback) {
+            Logger::getInstance()->setCallback(callback);
+        }
+    }
+}
diff --git a/src/libappimage/utils/Logger.h b/src/libappimage/utils/Logger.h
new file mode 100644 (file)
index 0000000..65f70f6
--- /dev/null
@@ -0,0 +1,71 @@
+#pragma once
+
+// system
+#include <memory>
+#include <functional>
+
+// local
+#include <appimage/utils/logging.h>
+
+namespace appimage {
+    namespace utils {
+        /**
+         * Provides a global logger to be used in the libappimage context. It follows the singleton pattern.
+         */
+        class Logger {
+        public:
+            /**
+             * @brief Set a custom logging function.
+             * Allows to capture the libappimage log messages.
+             *
+             * @param logging function
+             */
+            void setCallback(const log_callback_t& callback);
+
+
+            /**
+             * Calls the default logging function or the one set by the user by means of "setFunction()".
+             * @param level
+             * @param message
+             */
+            void log(const utils::LogLevel& level, const std::string& message);
+
+            /**
+             * Utility function to directly generate a debug message.
+             * @param message
+             */
+            static void debug(const std::string& message);
+
+            /**
+             * Utility function to directly generate a info message.
+             * @param message
+             */
+            static void info(const std::string& message);
+
+            /**
+             * Utility function to directly generate a warning message.
+             * @param message
+             */
+            static void warning(const std::string& message);
+
+            /**
+             * Utility function to directly generate a warning message.
+             * @param message
+             */
+            static void error(const std::string &message);
+
+            /**
+             * @return an instance of Logger
+             */
+            static Logger* getInstance();
+
+        private:
+            // Singleton pattern, use getInstance or the convenience logging methods instead.
+            Logger();
+
+            // PImpl
+            class Priv;
+            std::unique_ptr<Priv> d;
+        };
+    }
+}
diff --git a/src/libappimage/utils/MagicBytesChecker.cpp b/src/libappimage/utils/MagicBytesChecker.cpp
new file mode 100644 (file)
index 0000000..2a4e884
--- /dev/null
@@ -0,0 +1,82 @@
+// system
+#include <cstring>
+#include <algorithm>
+#include <iterator>
+#include <fstream>
+#include <vector>
+
+// local
+#include "MagicBytesChecker.h"
+
+using namespace appimage::utils;
+
+MagicBytesChecker::MagicBytesChecker(const std::string& path) : input(path, std::ios_base::binary) {}
+
+bool MagicBytesChecker::hasIso9660Signature() {
+    /* Implementation of the signature matches expressed at https://www.garykessler.net/library/file_sigs.html
+     * Signature: 43 44 30 30 31               = "CD001"
+     * ISO             ISO-9660 CD Disc Image
+     * This signature usually occurs at byte offset 32769 (0x8001),
+     * 34817 (0x8801), or 36865 (0x9001).
+     * More information can be found at MacTech or at ECMA.
+     */
+
+    if (input) {
+        off_t positions[] = {32769, 34817, 36865};
+        std::vector<char> signature = {'C', 'D', '0', '0', '1'};
+        for (int i = 0; i < 3; i++)
+            if (hasSignatureAt(input, signature, positions[i]))
+                return true;
+    }
+
+    return false;
+}
+
+bool MagicBytesChecker::hasElfSignature() {
+    if (input) {
+        // check magic hex 0x7f 0x45 0x4c 0x46 at offset 0
+        std::vector<char> signature = {0x7f, 0x45, 0x4c, 0x46};
+        if (hasSignatureAt(input, signature, 0))
+            return true;
+    }
+    return false;
+}
+
+bool MagicBytesChecker::hasAppImageType1Signature() {
+    if (input) {
+        // check magic hex 0x414901 at offset 8
+        std::vector<char> signature = {0x41, 0x49, 0x01};
+        if (hasSignatureAt(input, signature, 8))
+            return true;
+    }
+    return false;
+}
+
+bool MagicBytesChecker::hasAppImageType2Signature() {
+    if (input) {
+        // check magic hex 0x414902 at offset 8
+        std::vector<char> signature = {0x41, 0x49, 0x02};
+        if (hasSignatureAt(input, signature, 8))
+            return true;
+    }
+    return false;
+}
+
+bool MagicBytesChecker::hasSignatureAt(std::ifstream& input, std::vector<char>& signature, off_t offset) {
+    try {
+        // move to the right offset in the file
+        input.seekg(offset, std::ios_base::beg);
+
+        for (int i = 0; i < signature.size() && input; i++) {
+            if (signature[i] != input.get())
+                return false;
+        }
+
+        if (input)
+            return true;
+        else
+            return false;
+    } catch (const std::ios_base::failure&) {}
+
+    return false;
+}
diff --git a/src/libappimage/utils/MagicBytesChecker.h b/src/libappimage/utils/MagicBytesChecker.h
new file mode 100644 (file)
index 0000000..58cc66e
--- /dev/null
@@ -0,0 +1,39 @@
+#pragma once
+
+// system
+#include <string>
+#include <fstream>
+#include <vector>
+
+namespace appimage {
+    namespace utils {
+        /**
+         * Allows the verification of magic bytes at in a given file.
+         */
+        class MagicBytesChecker {
+        public:
+            explicit MagicBytesChecker(const std::string& path);
+
+            bool hasIso9660Signature();
+
+            bool hasElfSignature();
+
+            bool hasAppImageType1Signature();
+
+            bool hasAppImageType2Signature();
+
+        private:
+            std::ifstream input;
+
+            /**
+             * Verify if the input matches at <offset> with <signature>
+             * @param input
+             * @param signature
+             * @param offset
+             * @return true if there is a match, flase otherwise
+             */
+            bool hasSignatureAt(std::ifstream& input, std::vector<char>& signature, off_t offset);
+        };
+    }
+
+}
diff --git a/src/libappimage/utils/StringSanitizer.cpp b/src/libappimage/utils/StringSanitizer.cpp
new file mode 100644 (file)
index 0000000..ab155aa
--- /dev/null
@@ -0,0 +1,50 @@
+// STL includes
+#include <algorithm>
+#include <utility>
+#include <vector>
+
+// local includes
+#include "StringSanitizer.h"
+
+// initialize static const variables
+const std::initializer_list<std::string::value_type> StringSanitizer::asciiLetters_ = {
+    'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
+    'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
+    'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
+    'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
+};
+const std::initializer_list<std::string::value_type> StringSanitizer::asciiDigits_ = {
+    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+};
+const std::initializer_list<std::string::value_type> StringSanitizer::pathSafeChars_ = {
+    '.', '-', '_'
+};
+
+StringSanitizer::StringSanitizer(std::string input) : input_(std::move(input)) {};
+
+std::string StringSanitizer::sanitizeForPath() {
+    // output buffer
+    std::vector<std::string::value_type> buffer{};
+    buffer.reserve(input_.size());
+
+    // first of all, we compose an alphabet of safe characters
+    // all characters not contained in this alphabet will be replaced by some safe character, e.g., an underscore (_)
+    std::vector<std::string::value_type> safeAlphabet(asciiDigits_.size() + asciiLetters_.size() + pathSafeChars_.size());
+    for (const auto& partialAlphabet : {asciiDigits_, asciiLetters_, pathSafeChars_}) {
+        std::copy(partialAlphabet.begin(), partialAlphabet.end(), std::back_inserter(safeAlphabet));
+    }
+
+    for (auto c : input_) {
+        // replace if c is not an element of the safe alphabet
+        if (std::find(safeAlphabet.begin(), safeAlphabet.end(), c) == safeAlphabet.end()) {
+            c = '_';
+        }
+
+        buffer.emplace_back(c);
+    }
+
+    // C strings, anyone?
+    buffer.emplace_back('\0');
+
+    return std::string{buffer.data()};
+}
diff --git a/src/libappimage/utils/StringSanitizer.h b/src/libappimage/utils/StringSanitizer.h
new file mode 100644 (file)
index 0000000..69dc24b
--- /dev/null
@@ -0,0 +1,32 @@
+#pragma once
+
+// STL includes
+#include <string>
+
+/**
+ * Sanitizes strings for different needs, such as e.g., for inclusion in filenames/paths.
+ */
+class StringSanitizer {
+private:
+    std::string input_;
+
+    // these three lists can be used to compose alphabets for sanitization
+    static const std::initializer_list<std::string::value_type> asciiLetters_;
+    static const std::initializer_list<std::string::value_type> asciiDigits_;
+    static const std::initializer_list<std::string::value_type> pathSafeChars_;
+
+public:
+    /**
+     * Default constructor.
+     * @param input string to sanitize
+     */
+    explicit StringSanitizer(std::string input);
+
+    /**
+     * Sanitizes given string so it is safe to embed it in a path.
+     * Replaces all "unsafe" characters with a safe one.
+     * The aim is to keep the string readable/understandable to the user.
+     * @return sanitized filename
+     */
+    std::string sanitizeForPath();
+};
diff --git a/src/libappimage/utils/UrlEncoder.cpp b/src/libappimage/utils/UrlEncoder.cpp
new file mode 100644 (file)
index 0000000..c7b7e5d
--- /dev/null
@@ -0,0 +1,33 @@
+// system
+#include <cctype>
+#include <iomanip>
+#include <sstream>
+#include <string>
+
+// local
+#include "UrlEncoder.h"
+
+namespace appimage {
+    namespace utils {
+        std::string UrlEncoder::encode(const std::string& value) {
+            std::ostringstream escaped;
+            escaped.fill('0');
+            escaped << std::hex;
+
+            for (const std::string::value_type &c: value) {
+                // Keep alphanumeric and other accepted characters intact
+                if (isalnum((unsigned char) c) || c == '-' || c == '_' || c == '.' || c == '~' || c == '/') {
+                    escaped << c;
+                    continue;
+                }
+
+                // Any other characters are percent-encoded
+                escaped << std::uppercase;
+                escaped << '%' << std::setw(2) << int((unsigned char) c);
+                escaped << std::nouppercase;
+            }
+
+            return escaped.str();
+        }
+    }
+}
diff --git a/src/libappimage/utils/UrlEncoder.h b/src/libappimage/utils/UrlEncoder.h
new file mode 100644 (file)
index 0000000..a4fa8c9
--- /dev/null
@@ -0,0 +1,24 @@
+#pragma once
+
+// system
+#include <string>
+#include <sstream>
+
+
+namespace appimage {
+    namespace utils {
+        /**
+         * Provides a minimal implementation of the Uniform Resource Identifiers (RFC 2396)
+         * See: https://tools.ietf.org/html/rfc2396
+         */
+        class UrlEncoder {
+        public:
+            /**
+             * Scape chars in <value> according to RFC 2396
+             * @param value
+             * @return
+             */
+            static std::string encode(const std::string& value);
+        };
+    }
+}
diff --git a/src/libappimage/utils/hashlib.cpp b/src/libappimage/utils/hashlib.cpp
new file mode 100644 (file)
index 0000000..aa83286
--- /dev/null
@@ -0,0 +1,57 @@
+// system
+#include <cstring>
+#include <vector>
+#include <sstream>
+#include <iomanip>
+#include <iostream>
+
+// local
+#include "hashlib.h"
+// libraries
+extern "C" {
+#include "md5.h"
+}
+
+namespace appimage {
+    namespace utils {
+        namespace hashlib {
+
+            std::vector<uint8_t> md5(std::istream& data) {
+                Md5Context md5_context;
+                Md5Initialise(&md5_context);
+
+                // uint32_t type used to ensure that it's the maximum value
+                static const uint32_t chunk_size = 4096;
+                std::vector<char> buf(chunk_size);
+
+                uint32_t readCount;
+                while (data.read(buf.data(), buf.size()) || (readCount = static_cast<uint32_t>(data.gcount())) != 0)
+                    Md5Update(&md5_context, buf.data(), readCount); // feed buffer into checksum calculation
+
+                // Finalise computation
+                MD5_HASH checksum;
+                Md5Finalise(&md5_context, &checksum);
+
+                // Extract digest string
+                std::vector<uint8_t> digest(16);
+                memcpy(digest.data(), (const char*) checksum.bytes, 16);
+
+                return digest;
+            }
+
+            std::vector<uint8_t> md5(const std::string& data) {
+                std::stringstream ss(data);
+                return md5(ss);
+            }
+
+            std::string toHex(std::vector<uint8_t> digest) {
+                std::stringstream stream;
+                stream << std::hex << std::setfill('0');
+                for (const unsigned char& i : digest)
+                    stream << std::setw(2) << static_cast<unsigned>(i);
+
+                return stream.str();
+            }
+        }
+    }
+}
diff --git a/src/libappimage/utils/hashlib.h b/src/libappimage/utils/hashlib.h
new file mode 100644 (file)
index 0000000..089ee5c
--- /dev/null
@@ -0,0 +1,36 @@
+#pragma once
+
+// system
+#include <istream>
+
+
+
+namespace appimage {
+    namespace utils {
+        /**
+         * C++ wrapper around the bare C hashing algorithms implementations
+         */
+        namespace hashlib {
+            /**
+             * Convenience function to compute md5 sums from a std::istream
+             * @param data
+             * @return md5 sum on success, empty string otherwise
+             */
+            std::vector<uint8_t> md5(std::istream& data);
+
+            /**
+             * Convenience function to compute md5 sums from a std::string
+             * @param data
+             * @return md5 sum on success, empty string otherwise
+             */
+            std::vector<uint8_t> md5(const std::string& data);
+
+            /**
+             * Generates an hexadecimal representation of the values at <digest>
+             * @param digest
+             * @return hexadecimal representation of the values at <digest>
+             */
+            std::string toHex(std::vector<uint8_t> digest);
+        };
+    }
+}
diff --git a/src/libappimage/utils/light_byteswap.h b/src/libappimage/utils/light_byteswap.h
new file mode 100644 (file)
index 0000000..d0b98ba
--- /dev/null
@@ -0,0 +1,13 @@
+#pragma once
+
+#define bswap_16(value) \
+((((value) & 0xff) << 8) | ((value) >> 8))
+
+#define bswap_32(value) \
+(((uint32_t)bswap_16((uint16_t)((value) & 0xffff)) << 16) | \
+(uint32_t)bswap_16((uint16_t)((value) >> 16)))
+
+#define bswap_64(value) \
+(((uint64_t)bswap_32((uint32_t)((value) & 0xffffffff)) \
+<< 32) | \
+(uint64_t)bswap_32((uint32_t)((value) >> 32)))
diff --git a/src/libappimage/utils/light_elf.h b/src/libappimage/utils/light_elf.h
new file mode 100644 (file)
index 0000000..d6044a7
--- /dev/null
@@ -0,0 +1,119 @@
+/* 
+ * 
+ *  Linux kernel
+ *  Copyright (C) 2017 Linus Torvalds
+ *  Modified work Copyright (C) 2017 @teras (https://github.com/teras)
+ *  (Shortened version -- original work found here:
+ *   https://github.com/torvalds/linux/blob/master/include/uapi/linux/elf.h)
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License.
+ *
+ *  This program 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 General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ * 
+ */
+
+#ifndef LIGHT_ELF_H
+#define LIGHT_ELF_H
+
+#include <inttypes.h>
+
+__BEGIN_DECLS
+
+typedef uint16_t Elf32_Half;
+typedef uint16_t Elf64_Half;
+typedef uint32_t Elf32_Word;
+typedef uint32_t Elf64_Word;
+typedef uint64_t Elf64_Xword;
+typedef uint32_t Elf32_Addr;
+typedef uint64_t Elf64_Addr;
+typedef uint32_t Elf32_Off;
+typedef uint64_t Elf64_Off;
+
+#define EI_NIDENT 16
+
+typedef struct elf32_hdr {
+    unsigned char e_ident[EI_NIDENT];
+    Elf32_Half e_type;
+    Elf32_Half e_machine;
+    Elf32_Word e_version;
+    Elf32_Addr e_entry; /* Entry point */
+    Elf32_Off e_phoff;
+    Elf32_Off e_shoff;
+    Elf32_Word e_flags;
+    Elf32_Half e_ehsize;
+    Elf32_Half e_phentsize;
+    Elf32_Half e_phnum;
+    Elf32_Half e_shentsize;
+    Elf32_Half e_shnum;
+    Elf32_Half e_shstrndx;
+} Elf32_Ehdr;
+
+typedef struct elf64_hdr {
+    unsigned char e_ident[EI_NIDENT]; /* ELF "magic number" */
+    Elf64_Half e_type;
+    Elf64_Half e_machine;
+    Elf64_Word e_version;
+    Elf64_Addr e_entry; /* Entry point virtual address */
+    Elf64_Off e_phoff; /* Program header table file offset */
+    Elf64_Off e_shoff; /* Section header table file offset */
+    Elf64_Word e_flags;
+    Elf64_Half e_ehsize;
+    Elf64_Half e_phentsize;
+    Elf64_Half e_phnum;
+    Elf64_Half e_shentsize;
+    Elf64_Half e_shnum;
+    Elf64_Half e_shstrndx;
+} Elf64_Ehdr;
+
+typedef struct elf32_shdr {
+    Elf32_Word sh_name;
+    Elf32_Word sh_type;
+    Elf32_Word sh_flags;
+    Elf32_Addr sh_addr;
+    Elf32_Off sh_offset;
+    Elf32_Word sh_size;
+    Elf32_Word sh_link;
+    Elf32_Word sh_info;
+    Elf32_Word sh_addralign;
+    Elf32_Word sh_entsize;
+} Elf32_Shdr;
+
+typedef struct elf64_shdr {
+    Elf64_Word sh_name; /* Section name, index in string tbl */
+    Elf64_Word sh_type; /* Type of section */
+    Elf64_Xword sh_flags; /* Miscellaneous section attributes */
+    Elf64_Addr sh_addr; /* Section virtual addr at execution */
+    Elf64_Off sh_offset; /* Section file offset */
+    Elf64_Xword sh_size; /* Size of section in bytes */
+    Elf64_Word sh_link; /* Index of another section */
+    Elf64_Word sh_info; /* Additional section information */
+    Elf64_Xword sh_addralign; /* Section alignment */
+    Elf64_Xword sh_entsize; /* Entry size if section holds table */
+} Elf64_Shdr;
+
+/* Note header in a PT_NOTE section */
+typedef struct elf32_note {
+    Elf32_Word n_namesz; /* Name size */
+    Elf32_Word n_descsz; /* Content size */
+    Elf32_Word n_type; /* Content type */
+} Elf32_Nhdr;
+
+#define ELFCLASS32  1
+#define ELFDATA2LSB 1
+#define ELFDATA2MSB 2
+#define ELFCLASS64  2
+#define EI_CLASS    4
+#define EI_DATA     5
+
+__END_DECLS
+
+#endif /* elf.h */
diff --git a/src/libappimage/utils/path_utils.cpp b/src/libappimage/utils/path_utils.cpp
new file mode 100644 (file)
index 0000000..9a4f70d
--- /dev/null
@@ -0,0 +1,39 @@
+//system
+#include <string>
+
+// libraries
+#include <boost/filesystem.hpp>
+
+// local
+#include "path_utils.h"
+#include "hashlib.h"
+
+namespace bf = boost::filesystem;
+
+namespace appimage {
+    namespace utils {
+        std::string pathToURI(const std::string& path) {
+            if (path.compare(0, 7, "file://") != 0)
+                return "file://" + path;
+            else
+                return path;
+        }
+
+        std::string hashPath(const bf::path& path) {
+            if (path.empty())
+                return {};
+
+            const auto& canonicalPath = bf::absolute(path);
+
+            if (canonicalPath.empty())
+                return {};
+
+            auto uri = pathToURI(canonicalPath.string());
+
+            const auto md5raw = hashlib::md5(uri);
+            const auto md5Str = hashlib::toHex(md5raw);
+
+            return md5Str;
+        }
+    }
+}
diff --git a/src/libappimage/utils/path_utils.h b/src/libappimage/utils/path_utils.h
new file mode 100644 (file)
index 0000000..cc14b95
--- /dev/null
@@ -0,0 +1,31 @@
+#pragma once
+
+// system
+#include <string>
+
+// libraries
+#include <boost/filesystem.hpp>
+
+namespace appimage {
+    namespace utils {
+        /**
+         * Prepends 'file://' to a local path string if required.
+         * @param path
+         * @return
+         */
+        std::string pathToURI(const std::string& path);
+
+        /**
+         * @brief Provides a MD5 hash that identifies a file given its <path>.
+         *
+         * Implementation of the thumbnail filename hash function available at:
+         * https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html#THUMBSAVE
+         *
+         * It's may be used to identify files that are related to a given AppImage at a given location.
+         *
+         * @param path
+         * @return file hash
+         */
+        std::string hashPath(const boost::filesystem::path& path);
+    }
+}
diff --git a/src/libappimage/utils/resources_extractor/PayloadEntriesCache.cpp b/src/libappimage/utils/resources_extractor/PayloadEntriesCache.cpp
new file mode 100644 (file)
index 0000000..dab1fdc
--- /dev/null
@@ -0,0 +1,72 @@
+// local
+#include "PayloadEntriesCache.h"
+
+namespace appimage {
+    namespace utils {
+        PayloadEntriesCache::PayloadEntriesCache(const core::AppImage& image) : appImage(image) {
+            buildCache();
+        }
+
+        std::vector<std::string> PayloadEntriesCache::getEntriesPaths() const {
+            std::vector<std::string> paths;
+            for (const auto& item: entriesCache)
+                paths.emplace_back(item.first);
+
+            return paths;
+        }
+
+        appimage::core::PayloadEntryType PayloadEntriesCache::getEntryType(const std::string& path) const {
+            auto itr = entriesCache.find(path);
+            if (itr == entriesCache.end())
+                throw core::PayloadIteratorError("Entry doesn't exists: " + path);
+
+            return itr->second;
+        }
+
+        std::string PayloadEntriesCache::getEntryLinkTarget(const std::string& path) const {
+            auto itr = linksCache.find(path);
+            if (itr == linksCache.end())
+                throw core::PayloadIteratorError("Not a link: " + path);
+
+            if (itr->second.empty())
+                throw core::PayloadIteratorError("Loop found: " + path);
+            else
+                return itr->second;
+        }
+
+        void PayloadEntriesCache::buildCache() {
+            readAllEntries();
+            resolveLinks();
+        }
+
+        void PayloadEntriesCache::resolveLinks() {
+            for (auto itr = linksCache.begin(); itr != linksCache.end(); ++itr) {
+                auto target = itr->second;
+                auto jumpItr = linksCache.find(itr->second);
+
+                // follow links to the final target
+                while (jumpItr != linksCache.end() && jumpItr != itr) {
+                    target = jumpItr->second;
+                    jumpItr = linksCache.find(jumpItr->second);
+                }
+
+                // disable loops
+                if (target == itr->first)
+                    target = "";
+
+                // update cache
+                itr->second = target;
+            }
+        }
+
+        void PayloadEntriesCache::readAllEntries() {
+            for (auto fileItr = appImage.files(); fileItr != fileItr.end(); ++fileItr) {
+                entriesCache[fileItr.path()] = fileItr.type();
+
+                if (fileItr.type() == core::PayloadEntryType::LINK)
+                    linksCache[fileItr.path()] = fileItr.linkTarget();
+            }
+        }
+
+    }
+}
diff --git a/src/libappimage/utils/resources_extractor/PayloadEntriesCache.h b/src/libappimage/utils/resources_extractor/PayloadEntriesCache.h
new file mode 100644 (file)
index 0000000..c0b79b8
--- /dev/null
@@ -0,0 +1,72 @@
+#pragma once
+// system
+#include <map>
+#include <string>
+
+// local
+#include <appimage/core/AppImage.h>
+#include <appimage/core/exceptions.h>
+
+
+namespace appimage {
+    namespace utils {
+        /**
+         * Builds a cache of the entries contained in the AppImage payload. Include the entries path,
+         * type and in case of a link link entry the link target. Solve links chains in order to ease
+         * the lookup.
+         */
+        class PayloadEntriesCache {
+        public:
+            explicit PayloadEntriesCache(const core::AppImage& appImage);
+
+            /**
+             * @return entries path inside the AppImage Payload
+             */
+            std::vector<std::string> getEntriesPaths() const;
+
+            /**
+             * Get the type of the entry pointed by <path>
+             * @param path
+             * @return  entry type
+             * @throw PayloadIteratorError if <path> don't point to a existent entry
+             */
+            appimage::core::PayloadEntryType getEntryType(const std::string& path) const;
+
+            /**
+             * Get the final target if a given link entry.
+             *
+             * Final means that it will point to a regular entry.
+             *
+             * @param path
+             * @return final link target
+             * @throw PayloadIteratorError in case links of cycle
+             */
+            std::string getEntryLinkTarget(const std::string& path) const;
+
+        private:
+            core::AppImage appImage;
+            std::map<std::string, std::string> linksCache;
+            std::map<std::string, appimage::core::PayloadEntryType> entriesCache;
+
+            /**
+             * Iterate over all the entries in the AppImage and store all the link type entries
+             * and their targets inside linksCache.
+             */
+            void buildCache();
+
+            /**
+             * Fill linksCache with the link file paths and their target
+             */
+            void readAllEntries();
+
+            /**
+             * Resolve links chains to ease the link target lookup.
+             *
+             * Example scenario:
+             * A links to B links to C will be translated to: A links to C
+             * */
+            void resolveLinks();
+        };
+    }
+}
+
diff --git a/src/libappimage/utils/resources_extractor/ResourcesExtractor.cpp b/src/libappimage/utils/resources_extractor/ResourcesExtractor.cpp
new file mode 100644 (file)
index 0000000..5f10934
--- /dev/null
@@ -0,0 +1,185 @@
+// system
+#include <map>
+#include <set>
+#include <fstream>
+
+// libraries
+#include <boost/filesystem.hpp>
+#include <XdgUtils/DesktopEntry/DesktopEntry.h>
+
+// local
+#include <appimage/core/PayloadEntryType.h>
+#include <appimage/desktop_integration/exceptions.h>
+#include "PayloadEntriesCache.h"
+#include <appimage/utils/ResourcesExtractor.h>
+
+
+using namespace XdgUtils::DesktopEntry;
+using namespace appimage::core;
+namespace bf = boost::filesystem;
+
+namespace appimage {
+    namespace utils {
+        class ResourcesExtractor::Priv {
+        public:
+            explicit Priv(const AppImage& appImage) : appImage(appImage), entriesCache(appImage) {}
+
+
+            core::AppImage appImage;
+
+            PayloadEntriesCache entriesCache;
+
+            static bool isIconFile(const std::string& fileName) {
+                return (fileName.find("usr/share/icons") != std::string::npos);
+            }
+
+            static bool isMainDesktopFile(const std::string& fileName) {
+                return fileName.find(".desktop") != std::string::npos &&
+                       fileName.find('/') == std::string::npos;
+            }
+
+            static bool isMimeFile(const std::string& fileName) {
+                const std::string prefix = "usr/share/mime/packages/";
+                const std::string suffix = ".xml";
+
+                return !fileName.compare(0, prefix.size(), prefix) &&
+                       !fileName.compare(fileName.size() - suffix.size(), suffix.size(), suffix) &&
+                       fileName.size() > (prefix.size() + suffix.size());
+            }
+
+            static std::vector<char> readDataFile(std::istream& istream) {
+                return {std::istreambuf_iterator<char>(istream), std::istreambuf_iterator<char>()};
+            }
+
+            static std::string readTextFile(std::istream& istream) {
+                return {std::istreambuf_iterator<char>(istream), std::istreambuf_iterator<char>()};
+            }
+        };
+
+        ResourcesExtractor::ResourcesExtractor(const core::AppImage& appImage) : d(new Priv(appImage)) {}
+
+
+        std::vector<std::string> ResourcesExtractor::getIconFilePaths(const std::string& iconName) const {
+            std::vector<std::string> filePaths;
+
+            for (const auto& filePath: d->entriesCache.getEntriesPaths()) {
+                if (d->isIconFile(filePath) &&
+                    filePath.find(iconName) != std::string::npos) {
+                    filePaths.emplace_back(filePath);
+                }
+            }
+
+            return filePaths;
+        }
+
+        std::vector<std::string> ResourcesExtractor::getMimeTypePackagesPaths() const {
+            std::vector<std::string> filePaths;
+
+            for (const auto& filePath: d->entriesCache.getEntriesPaths()) {
+                if (d->isMimeFile(filePath))
+                    filePaths.emplace_back(filePath);
+            }
+
+            return filePaths;
+        }
+
+        void ResourcesExtractor::extractTo(const std::map<std::string, std::string>& targetsMap) const {
+            // resolve links to ensure proper extraction
+            std::map<std::string, std::string> realTargetsMap;
+
+            for (const auto& target : targetsMap) {
+                if (d->entriesCache.getEntryType(target.first) == PayloadEntryType::LINK) {
+                    const std::string& realTarget = d->entriesCache.getEntryLinkTarget(target.first);
+                    realTargetsMap[realTarget] = target.second;
+                } else {
+                    realTargetsMap.insert(target);
+                }
+            }
+
+            // we need to iterate over all file paths in the AppImage
+            for (auto sourceFileItr = d->appImage.files(); sourceFileItr != sourceFileItr.end(); ++sourceFileItr) {
+                // check if we have to extract the file by looking into the real targets map
+                const auto targetsMapEntry = realTargetsMap.find(sourceFileItr.path());
+
+                // if the file isn't relevant for us, we can skip it
+                if (targetsMapEntry == realTargetsMap.end()) {
+                    continue;
+                }
+
+                // begin extraction
+                bf::path targetPath(targetsMapEntry->second);
+
+                std::cout << "Extracting " << sourceFileItr.path() << " to " << targetPath << std::endl;
+
+                // create parent dirs
+                const auto parentDirPath = targetPath.parent_path();
+                bf::create_directories(parentDirPath);
+
+                // write file contents
+                std::ofstream file(targetPath.string());
+                file << sourceFileItr.read().rdbuf();
+
+                file.close();
+            }
+        }
+
+        std::vector<char> ResourcesExtractor::extract(const std::string& path) const {
+            // Resolve any link before extracting the file
+            auto regularEntryPath = path;
+            if (d->entriesCache.getEntryType(path) == PayloadEntryType::LINK)
+                regularEntryPath = d->entriesCache.getEntryLinkTarget(path);
+
+            for (auto fileItr = d->appImage.files(); fileItr != fileItr.end(); ++fileItr) {
+                if (fileItr.path() == regularEntryPath)
+                    return d->readDataFile(fileItr.read());
+            }
+
+            throw core::PayloadIteratorError("Entry doesn't exists: " + path);
+        }
+
+        std::map<std::string, std::vector<char>>
+        ResourcesExtractor::extract(const std::vector<std::string>& paths) const {
+            // Resolve any link before extracting the files and keep a reference to the original path
+            std::map<std::string, std::string> reverseLinks;
+            for (const auto& path: paths)
+                if (d->entriesCache.getEntryType(path) == PayloadEntryType::LINK)
+                    reverseLinks[d->entriesCache.getEntryLinkTarget(path)] = path;
+                else
+                    reverseLinks[path] = path;
+
+            std::map<std::string, std::vector<char>> result;
+            for (auto fileItr = d->appImage.files(); fileItr != fileItr.end(); ++fileItr) {
+                auto itr = reverseLinks.find(fileItr.path());
+
+                // extract the file data and store it using the original path
+                if (itr != reverseLinks.end())
+                    result[itr->second] = d->readDataFile(fileItr.read());
+            }
+
+            return result;
+
+        }
+
+        std::string ResourcesExtractor::extractText(const std::string& path) const {
+            // Resolve any link before extracting the file
+            auto regularEntryPath = path;
+            if (d->entriesCache.getEntryType(path) == PayloadEntryType::LINK)
+                regularEntryPath = d->entriesCache.getEntryLinkTarget(path);
+
+            for (auto fileItr = d->appImage.files(); fileItr != fileItr.end(); ++fileItr) {
+                if (fileItr.path() == regularEntryPath)
+                    return d->readTextFile(fileItr.read());
+            }
+
+            throw core::PayloadIteratorError("Entry doesn't exists: " + path);
+        }
+
+        std::string ResourcesExtractor::getDesktopEntryPath() const {
+            for (auto fileItr = d->appImage.files(); fileItr != fileItr.end(); ++fileItr)
+                if (d->isMainDesktopFile(fileItr.path()))
+                    return fileItr.path();
+
+            throw AppImageError("Missing Desktop Entry");
+        }
+    }
+}
diff --git a/src/libappimage_hashlib/CMakeLists.txt b/src/libappimage_hashlib/CMakeLists.txt
new file mode 100644 (file)
index 0000000..5e59014
--- /dev/null
@@ -0,0 +1,17 @@
+set(CMAKE_POSITION_INDEPENDENT_CODE ON)
+
+set(public_header ${CMAKE_CURRENT_SOURCE_DIR}/include/hashlib.h ../../include/appimage/appimage_legacy.h)
+
+add_library(libappimage_hashlib STATIC md5.c ${public_header})
+set_target_properties(libappimage_hashlib PROPERTIES PREFIX "")
+target_include_directories(libappimage_hashlib
+    PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include/>
+)
+
+# install libappimage
+install(TARGETS libappimage_hashlib
+    EXPORT libappimageTargets
+    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT libappimage
+    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT libappimage
+    PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/appimage COMPONENT libappimage-dev
+)
diff --git a/src/libappimage_hashlib/include/hashlib.h b/src/libappimage_hashlib/include/hashlib.h
new file mode 100644 (file)
index 0000000..9801b78
--- /dev/null
@@ -0,0 +1,4 @@
+#pragma once
+
+// include implementations
+#include "md5.h"
diff --git a/src/libappimage_hashlib/include/md5.h b/src/libappimage_hashlib/include/md5.h
new file mode 100644 (file)
index 0000000..ede1546
--- /dev/null
@@ -0,0 +1,34 @@
+#pragma once
+
+
+#include <stdint.h>
+#include <stdio.h>
+
+typedef struct {
+    uint32_t lo;
+    uint32_t hi;
+    uint32_t a;
+    uint32_t b;
+    uint32_t c;
+    uint32_t d;
+    uint8_t  buffer[64];
+    uint32_t block[16];
+} Md5Context;
+
+#define MD5_HASH_SIZE (128 / 8)
+
+typedef struct {
+    uint8_t bytes[MD5_HASH_SIZE];
+} MD5_HASH;
+
+// initialize new context
+void Md5Initialise(Md5Context* ctx);
+
+// add data to the context
+void Md5Update(Md5Context* ctx, void const* buf, uint32_t bufSize);
+
+// calculate final digest from context
+void Md5Finalise(Md5Context* ctx, MD5_HASH* digest);
+
+// create new context, add data from buffer to it, and calculate digest
+void Md5Calculate(void const* Buffer, uint32_t BufferSize, MD5_HASH* Digest);
diff --git a/src/libappimage_hashlib/md5.c b/src/libappimage_hashlib/md5.c
new file mode 100644 (file)
index 0000000..bb3cf18
--- /dev/null
@@ -0,0 +1,335 @@
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//  WjCryptLib_Md5
+//
+//  Implementation of MD5 hash function. Originally written by Alexander Peslyak. Modified by WaterJuice retaining
+//  Public Domain license.
+//
+//  This is free and unencumbered software released into the public domain - June 2013 waterjuice.org
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//  IMPORTS
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+#include "md5.h"
+#include <memory.h>
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//  INTERNAL FUNCTIONS
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//  F, G, H, I
+//
+//  The basic MD5 functions. F and G are optimised compared to their RFC 1321 definitions for architectures that lack
+//  an AND-NOT instruction, just like in Colin Plumb's implementation.
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#define F( x, y, z )            ( (z) ^ ((x) & ((y) ^ (z))) )
+#define G( x, y, z )            ( (y) ^ ((z) & ((x) ^ (y))) )
+#define H( x, y, z )            ( (x) ^ (y) ^ (z) )
+#define I( x, y, z )            ( (y) ^ ((x) | ~(z)) )
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//  STEP
+//
+//  The MD5 transformation for all four rounds.
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#define STEP( f, a, b, c, d, x, t, s )                          \
+    (a) += f((b), (c), (d)) + (x) + (t);                        \
+    (a) = (((a) << (s)) | (((a) & 0xffffffff) >> (32 - (s))));  \
+    (a) += (b);
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//  TransformFunction
+//
+//  This processes one or more 64-byte data blocks, but does NOT update the bit counters. There are no alignment
+//  requirements.
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+static
+void*
+TransformFunction
+    (
+        Md5Context*     ctx,
+        void const*     data,
+        uintmax_t       size
+    )
+{
+    uint8_t*     ptr;
+    uint32_t     a;
+    uint32_t     b;
+    uint32_t     c;
+    uint32_t     d;
+    uint32_t     saved_a;
+    uint32_t     saved_b;
+    uint32_t     saved_c;
+    uint32_t     saved_d;
+
+    #define GET(n) (ctx->block[(n)])
+    #define SET(n) (ctx->block[(n)] =             \
+            ((uint32_t)ptr[(n)*4 + 0] << 0 )      \
+        |   ((uint32_t)ptr[(n)*4 + 1] << 8 )      \
+        |   ((uint32_t)ptr[(n)*4 + 2] << 16)      \
+        |   ((uint32_t)ptr[(n)*4 + 3] << 24) )
+
+    ptr = (uint8_t*)data;
+
+    a = ctx->a;
+    b = ctx->b;
+    c = ctx->c;
+    d = ctx->d;
+
+    do
+    {
+        saved_a = a;
+        saved_b = b;
+        saved_c = c;
+        saved_d = d;
+
+        // Round 1
+        STEP( F, a, b, c, d, SET(0),  0xd76aa478, 7 )
+        STEP( F, d, a, b, c, SET(1),  0xe8c7b756, 12 )
+        STEP( F, c, d, a, b, SET(2),  0x242070db, 17 )
+        STEP( F, b, c, d, a, SET(3),  0xc1bdceee, 22 )
+        STEP( F, a, b, c, d, SET(4),  0xf57c0faf, 7 )
+        STEP( F, d, a, b, c, SET(5),  0x4787c62a, 12 )
+        STEP( F, c, d, a, b, SET(6),  0xa8304613, 17 )
+        STEP( F, b, c, d, a, SET(7),  0xfd469501, 22 )
+        STEP( F, a, b, c, d, SET(8 ),  0x698098d8, 7 )
+        STEP( F, d, a, b, c, SET(9 ),  0x8b44f7af, 12 )
+        STEP( F, c, d, a, b, SET(10 ), 0xffff5bb1, 17 )
+        STEP( F, b, c, d, a, SET(11 ), 0x895cd7be, 22 )
+        STEP( F, a, b, c, d, SET(12 ), 0x6b901122, 7 )
+        STEP( F, d, a, b, c, SET(13 ), 0xfd987193, 12 )
+        STEP( F, c, d, a, b, SET(14 ), 0xa679438e, 17 )
+        STEP( F, b, c, d, a, SET(15 ), 0x49b40821, 22 )
+
+        // Round 2
+        STEP( G, a, b, c, d, GET(1),  0xf61e2562, 5 )
+        STEP( G, d, a, b, c, GET(6),  0xc040b340, 9 )
+        STEP( G, c, d, a, b, GET(11), 0x265e5a51, 14 )
+        STEP( G, b, c, d, a, GET(0),  0xe9b6c7aa, 20 )
+        STEP( G, a, b, c, d, GET(5),  0xd62f105d, 5 )
+        STEP( G, d, a, b, c, GET(10), 0x02441453, 9 )
+        STEP( G, c, d, a, b, GET(15), 0xd8a1e681, 14 )
+        STEP( G, b, c, d, a, GET(4),  0xe7d3fbc8, 20 )
+        STEP( G, a, b, c, d, GET(9),  0x21e1cde6, 5 )
+        STEP( G, d, a, b, c, GET(14), 0xc33707d6, 9 )
+        STEP( G, c, d, a, b, GET(3),  0xf4d50d87, 14 )
+        STEP( G, b, c, d, a, GET(8),  0x455a14ed, 20 )
+        STEP( G, a, b, c, d, GET(13), 0xa9e3e905, 5 )
+        STEP( G, d, a, b, c, GET(2),  0xfcefa3f8, 9 )
+        STEP( G, c, d, a, b, GET(7),  0x676f02d9, 14 )
+        STEP( G, b, c, d, a, GET(12), 0x8d2a4c8a, 20 )
+
+        // Round 3
+        STEP( H, a, b, c, d, GET(5),  0xfffa3942, 4 )
+        STEP( H, d, a, b, c, GET(8),  0x8771f681, 11 )
+        STEP( H, c, d, a, b, GET(11), 0x6d9d6122, 16 )
+        STEP( H, b, c, d, a, GET(14), 0xfde5380c, 23 )
+        STEP( H, a, b, c, d, GET(1),  0xa4beea44, 4 )
+        STEP( H, d, a, b, c, GET(4),  0x4bdecfa9, 11 )
+        STEP( H, c, d, a, b, GET(7),  0xf6bb4b60, 16 )
+        STEP( H, b, c, d, a, GET(10), 0xbebfbc70, 23 )
+        STEP( H, a, b, c, d, GET(13), 0x289b7ec6, 4 )
+        STEP( H, d, a, b, c, GET(0),  0xeaa127fa, 11 )
+        STEP( H, c, d, a, b, GET(3),  0xd4ef3085, 16 )
+        STEP( H, b, c, d, a, GET(6),  0x04881d05, 23 )
+        STEP( H, a, b, c, d, GET(9),  0xd9d4d039, 4 )
+        STEP( H, d, a, b, c, GET(12), 0xe6db99e5, 11 )
+        STEP( H, c, d, a, b, GET(15), 0x1fa27cf8, 16 )
+        STEP( H, b, c, d, a, GET(2),  0xc4ac5665, 23 )
+
+        // Round 4
+        STEP( I, a, b, c, d, GET(0),  0xf4292244, 6 )
+        STEP( I, d, a, b, c, GET(7),  0x432aff97, 10 )
+        STEP( I, c, d, a, b, GET(14), 0xab9423a7, 15 )
+        STEP( I, b, c, d, a, GET(5),  0xfc93a039, 21 )
+        STEP( I, a, b, c, d, GET(12), 0x655b59c3, 6 )
+        STEP( I, d, a, b, c, GET(3),  0x8f0ccc92, 10 )
+        STEP( I, c, d, a, b, GET(10), 0xffeff47d, 15 )
+        STEP( I, b, c, d, a, GET(1),  0x85845dd1, 21 )
+        STEP( I, a, b, c, d, GET(8),  0x6fa87e4f, 6 )
+        STEP( I, d, a, b, c, GET(15), 0xfe2ce6e0, 10 )
+        STEP( I, c, d, a, b, GET(6),  0xa3014314, 15 )
+        STEP( I, b, c, d, a, GET(13), 0x4e0811a1, 21 )
+        STEP( I, a, b, c, d, GET(4),  0xf7537e82, 6 )
+        STEP( I, d, a, b, c, GET(11), 0xbd3af235, 10 )
+        STEP( I, c, d, a, b, GET(2),  0x2ad7d2bb, 15 )
+        STEP( I, b, c, d, a, GET(9),  0xeb86d391, 21 )
+
+        a += saved_a;
+        b += saved_b;
+        c += saved_c;
+        d += saved_d;
+
+        ptr += 64;
+    } while( size -= 64 );
+
+    ctx->a = a;
+    ctx->b = b;
+    ctx->c = c;
+    ctx->d = d;
+
+    #undef GET
+    #undef SET
+
+    return ptr;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//  EXPORTED FUNCTIONS
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//  Md5Initialise
+//
+//  Initialises an MD5 Context. Use this to initialise/reset a context.
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+void
+Md5Initialise
+    (
+        Md5Context*         Context         // [out]
+    )
+{
+    Context->a = 0x67452301;
+    Context->b = 0xefcdab89;
+    Context->c = 0x98badcfe;
+    Context->d = 0x10325476;
+
+    Context->lo = 0;
+    Context->hi = 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//  Md5Update
+//
+//  Adds data to the MD5 context. This will process the data and update the internal state of the context. Keep on
+//  calling this function until all the data has been added. Then call Md5Finalise to calculate the hash.
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+void
+Md5Update
+    (
+        Md5Context*         Context,        // [in out]
+        void const*         Buffer,         // [in]
+        uint32_t            BufferSize      // [in]
+    )
+{
+    uint32_t    saved_lo;
+    uint32_t    used;
+    uint32_t    free;
+
+    saved_lo = Context->lo;
+    if( (Context->lo = (saved_lo + BufferSize) & 0x1fffffff) < saved_lo )
+    {
+        Context->hi++;
+    }
+    Context->hi += (uint32_t)( BufferSize >> 29 );
+
+    used = saved_lo & 0x3f;
+
+    if( used )
+    {
+        free = 64 - used;
+
+        if( BufferSize < free )
+        {
+            memcpy( &Context->buffer[used], Buffer, BufferSize );
+            return;
+        }
+
+        memcpy( &Context->buffer[used], Buffer, free );
+        Buffer = (uint8_t*)Buffer + free;
+        BufferSize -= free;
+        TransformFunction(Context, Context->buffer, 64);
+    }
+
+    if( BufferSize >= 64 )
+    {
+        Buffer = TransformFunction( Context, Buffer, BufferSize & ~(unsigned long)0x3f );
+        BufferSize &= 0x3f;
+    }
+
+    memcpy( Context->buffer, Buffer, BufferSize );
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//  Md5Finalise
+//
+//  Performs the final calculation of the hash and returns the digest (16 byte buffer containing 128bit hash). After
+//  calling this, Md5Initialised must be used to reuse the context.
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+void
+Md5Finalise
+    (
+        Md5Context*         Context,        // [in out]
+        MD5_HASH*           Digest          // [in]
+    )
+{
+    uint32_t    used;
+    uint32_t    free;
+
+    used = Context->lo & 0x3f;
+
+    Context->buffer[used++] = 0x80;
+
+    free = 64 - used;
+
+    if(free < 8)
+    {
+        memset( &Context->buffer[used], 0, free );
+        TransformFunction( Context, Context->buffer, 64 );
+        used = 0;
+        free = 64;
+    }
+
+    memset( &Context->buffer[used], 0, free - 8 );
+
+    Context->lo <<= 3;
+    Context->buffer[56] = (uint8_t)( Context->lo );
+    Context->buffer[57] = (uint8_t)( Context->lo >> 8 );
+    Context->buffer[58] = (uint8_t)( Context->lo >> 16 );
+    Context->buffer[59] = (uint8_t)( Context->lo >> 24 );
+    Context->buffer[60] = (uint8_t)( Context->hi );
+    Context->buffer[61] = (uint8_t)( Context->hi >> 8 );
+    Context->buffer[62] = (uint8_t)( Context->hi >> 16 );
+    Context->buffer[63] = (uint8_t)( Context->hi >> 24 );
+
+    TransformFunction( Context, Context->buffer, 64 );
+
+    Digest->bytes[0]  = (uint8_t)( Context->a );
+    Digest->bytes[1]  = (uint8_t)( Context->a >> 8 );
+    Digest->bytes[2]  = (uint8_t)( Context->a >> 16 );
+    Digest->bytes[3]  = (uint8_t)( Context->a >> 24 );
+    Digest->bytes[4]  = (uint8_t)( Context->b );
+    Digest->bytes[5]  = (uint8_t)( Context->b >> 8 );
+    Digest->bytes[6]  = (uint8_t)( Context->b >> 16 );
+    Digest->bytes[7]  = (uint8_t)( Context->b >> 24 );
+    Digest->bytes[8]  = (uint8_t)( Context->c );
+    Digest->bytes[9]  = (uint8_t)( Context->c >> 8 );
+    Digest->bytes[10] = (uint8_t)( Context->c >> 16 );
+    Digest->bytes[11] = (uint8_t)( Context->c >> 24 );
+    Digest->bytes[12] = (uint8_t)( Context->d );
+    Digest->bytes[13] = (uint8_t)( Context->d >> 8 );
+    Digest->bytes[14] = (uint8_t)( Context->d >> 16 );
+    Digest->bytes[15] = (uint8_t)( Context->d >> 24 );
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//  Md5Calculate
+//
+//  Combines Md5Initialise, Md5Update, and Md5Finalise into one function. Calculates the MD5 hash of the buffer.
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+void
+Md5Calculate
+    (
+        void  const*        Buffer,         // [in]
+        uint32_t            BufferSize,     // [in]
+        MD5_HASH*           Digest          // [in]
+    )
+{
+    Md5Context context;
+
+    Md5Initialise( &context );
+    Md5Update( &context, Buffer, BufferSize );
+    Md5Finalise( &context, Digest );
+}
diff --git a/src/libappimage_shared/CMakeLists.txt b/src/libappimage_shared/CMakeLists.txt
new file mode 100644 (file)
index 0000000..ef7238b
--- /dev/null
@@ -0,0 +1,27 @@
+set(CMAKE_POSITION_INDEPENDENT_CODE ON)
+
+set(libappimage_shared_public_header ${PROJECT_SOURCE_DIR}/include/appimage/appimage_shared.h)
+
+add_library(libappimage_shared STATIC
+    ${libappimage_shared_public_header}
+    elf.c
+    hexlify.c
+    light_byteswap.h
+    light_elf.h
+    digest.c
+)
+set_target_properties(libappimage_shared PROPERTIES PREFIX "")
+target_include_directories(libappimage_shared PUBLIC
+    $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
+    $<INSTALL_INTERFACE:include>
+)
+set_property(TARGET libappimage_shared PROPERTY PUBLIC_HEADER ${libappimage_shared_public_header})
+target_link_libraries(libappimage_shared PRIVATE libappimage_hashlib)
+
+# install libappimage
+install(TARGETS libappimage_shared
+    EXPORT libappimageTargets
+    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT libappimage
+    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT libappimage
+    PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/appimage COMPONENT libappimage-dev
+)
diff --git a/src/libappimage_shared/digest.c b/src/libappimage_shared/digest.c
new file mode 100644 (file)
index 0000000..a9294b8
--- /dev/null
@@ -0,0 +1,139 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <appimage/appimage_shared.h>
+#include <hashlib.h>
+
+bool appimage_type2_digest_md5(const char* path, char* digest) {
+    // skip digest, signature and key sections in digest calculation
+    unsigned long digest_md5_offset = 0, digest_md5_length = 0;
+    if (!appimage_get_elf_section_offset_and_length(path, ".digest_md5", &digest_md5_offset, &digest_md5_length))
+        return false;
+
+    unsigned long signature_offset = 0, signature_length = 0;
+    if (!appimage_get_elf_section_offset_and_length(path, ".sha256_sig", &signature_offset, &signature_length))
+        return false;
+
+    unsigned long sig_key_offset = 0, sig_key_length = 0;
+    if (!appimage_get_elf_section_offset_and_length(path, ".sig_key", &sig_key_offset, &sig_key_length))
+        return false;
+
+    Md5Context md5_context;
+    Md5Initialise(&md5_context);
+
+    // read file in chunks
+    static const int chunk_size = 4096;
+
+    FILE *fp = fopen(path, "r");
+
+    // determine file size
+    fseek(fp, 0L, SEEK_END);
+    const long file_size = ftell(fp);
+    rewind(fp);
+
+    long bytes_left = file_size;
+
+    // if a section spans over more than a single chunk, we need emulate null bytes in the following chunks
+    ssize_t bytes_skip_following_chunks = 0;
+
+    while (bytes_left > 0) {
+        char buffer[chunk_size];
+
+        long current_position = ftell(fp);
+
+        ssize_t bytes_left_this_chunk = chunk_size;
+
+        // first, check whether there's bytes left that need to be skipped
+        if (bytes_skip_following_chunks > 0) {
+            ssize_t bytes_skip_this_chunk = (bytes_skip_following_chunks % chunk_size == 0) ? chunk_size : (bytes_skip_following_chunks % chunk_size);
+            bytes_left_this_chunk -= bytes_skip_this_chunk;
+
+            // we could just set it to 0 here, but it makes more sense to use -= for debugging
+            bytes_skip_following_chunks -= bytes_skip_this_chunk;
+
+            // make sure to skip these bytes in the file
+            fseek(fp, bytes_skip_this_chunk, SEEK_CUR);
+        }
+
+        // check whether there's a section in this chunk that we need to skip
+        if (digest_md5_offset != 0 && digest_md5_length != 0 && digest_md5_offset - current_position > 0 && digest_md5_offset - current_position < chunk_size) {
+            ssize_t begin_of_section = (digest_md5_offset - current_position) % chunk_size;
+            // read chunk before section
+            fread(buffer, sizeof(char), (size_t) begin_of_section, fp);
+
+            bytes_left_this_chunk -= begin_of_section;
+            bytes_left_this_chunk -= digest_md5_length;
+
+            // if bytes_left is now < 0, the section exceeds the current chunk
+            // this amount of bytes needs to be skipped in the future sections
+            if (bytes_left_this_chunk < 0) {
+                bytes_skip_following_chunks = (size_t) (-1 * bytes_left_this_chunk);
+                bytes_left_this_chunk = 0;
+            }
+
+            // if there's bytes left to read, we need to seek the difference between chunk's end and bytes_left
+            fseek(fp, (chunk_size - bytes_left_this_chunk - begin_of_section), SEEK_CUR);
+        }
+
+        // check whether there's a section in this chunk that we need to skip
+        if (signature_offset != 0 && signature_length != 0 && signature_offset - current_position > 0 && signature_offset - current_position < chunk_size) {
+            ssize_t begin_of_section = (signature_offset - current_position) % chunk_size;
+            // read chunk before section
+            fread(buffer, sizeof(char), (size_t) begin_of_section, fp);
+
+            bytes_left_this_chunk -= begin_of_section;
+            bytes_left_this_chunk -= signature_length;
+
+            // if bytes_left is now < 0, the section exceeds the current chunk
+            // this amount of bytes needs to be skipped in the future sections
+            if (bytes_left_this_chunk < 0) {
+                bytes_skip_following_chunks = (size_t) (-1 * bytes_left_this_chunk);
+                bytes_left_this_chunk = 0;
+            }
+
+            // if there's bytes left to read, we need to seek the difference between chunk's end and bytes_left
+            fseek(fp, (chunk_size - bytes_left_this_chunk - begin_of_section), SEEK_CUR);
+        }
+
+        // check whether there's a section in this chunk that we need to skip
+        if (sig_key_offset != 0 && sig_key_length != 0 && sig_key_offset - current_position > 0 && sig_key_offset - current_position < chunk_size) {
+            ssize_t begin_of_section = (sig_key_offset - current_position) % chunk_size;
+            // read chunk before section
+            fread(buffer, sizeof(char), (size_t) begin_of_section, fp);
+
+            bytes_left_this_chunk -= begin_of_section;
+            bytes_left_this_chunk -= sig_key_length;
+
+            // if bytes_left is now < 0, the section exceeds the current chunk
+            // this amount of bytes needs to be skipped in the future sections
+            if (bytes_left_this_chunk < 0) {
+                bytes_skip_following_chunks = (size_t) (-1 * bytes_left_this_chunk);
+                bytes_left_this_chunk = 0;
+            }
+
+            // if there's bytes left to read, we need to seek the difference between chunk's end and bytes_left
+            fseek(fp, (chunk_size - bytes_left_this_chunk - begin_of_section), SEEK_CUR);
+        }
+
+        // check whether we're done already
+        if (bytes_left_this_chunk > 0) {
+            // read data from file into buffer with the correct offset in case bytes have to be skipped
+            fread(buffer + (chunk_size - bytes_left_this_chunk), sizeof(char), (size_t) bytes_left_this_chunk, fp);
+        }
+
+        // feed buffer into checksum calculation
+        Md5Update(&md5_context, buffer, chunk_size);
+
+        bytes_left -= chunk_size;
+    }
+
+    MD5_HASH checksum;
+    Md5Finalise(&md5_context, &checksum);
+
+    memcpy(digest, (const char*) checksum.bytes, 16);
+
+    fclose(fp);
+
+    return true;
+}
diff --git a/src/libappimage_shared/elf.c b/src/libappimage_shared/elf.c
new file mode 100644 (file)
index 0000000..6e7168e
--- /dev/null
@@ -0,0 +1,217 @@
+#include <stdio.h>
+#include <stdint.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <fcntl.h>
+#include <stdbool.h>
+#include <memory.h>
+#include <sys/mman.h>
+
+#include "light_elf.h"
+#include "light_byteswap.h"
+
+
+typedef Elf32_Nhdr Elf_Nhdr;
+
+static char *fname;
+static Elf64_Ehdr ehdr;
+
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+#define ELFDATANATIVE ELFDATA2LSB
+#elif __BYTE_ORDER == __BIG_ENDIAN
+#define ELFDATANATIVE ELFDATA2MSB
+#else
+#error "Unknown machine endian"
+#endif
+
+static uint16_t file16_to_cpu(uint16_t val)
+{
+       if (ehdr.e_ident[EI_DATA] != ELFDATANATIVE)
+               val = bswap_16(val);
+       return val;
+}
+
+static uint32_t file32_to_cpu(uint32_t val)
+{
+       if (ehdr.e_ident[EI_DATA] != ELFDATANATIVE)
+               val = bswap_32(val);
+       return val;
+}
+
+static uint64_t file64_to_cpu(uint64_t val)
+{
+       if (ehdr.e_ident[EI_DATA] != ELFDATANATIVE)
+               val = bswap_64(val);
+       return val;
+}
+
+static off_t read_elf32(FILE* fd)
+{
+       Elf32_Ehdr ehdr32;
+       Elf32_Shdr shdr32;
+       off_t last_shdr_offset;
+       ssize_t ret;
+    off_t  sht_end, last_section_end;
+
+       fseeko(fd, 0, SEEK_SET);
+       ret = fread(&ehdr32, 1, sizeof(ehdr32), fd);
+       if (ret < 0 || (size_t)ret != sizeof(ehdr32)) {
+               fprintf(stderr, "Read of ELF header from %s failed: %s\n",
+                       fname, strerror(errno));
+               return -1;
+       }
+
+       ehdr.e_shoff            = file32_to_cpu(ehdr32.e_shoff);
+       ehdr.e_shentsize        = file16_to_cpu(ehdr32.e_shentsize);
+       ehdr.e_shnum            = file16_to_cpu(ehdr32.e_shnum);
+
+       last_shdr_offset = ehdr.e_shoff + (ehdr.e_shentsize * (ehdr.e_shnum - 1));
+    fseeko(fd, last_shdr_offset, SEEK_SET);
+    ret = fread(&shdr32, 1, sizeof(shdr32), fd);
+       if (ret < 0 || (size_t)ret != sizeof(shdr32)) {
+               fprintf(stderr, "Read of ELF section header from %s failed: %s\n",
+                       fname, strerror(errno));
+               return -1;
+       }
+
+       /* ELF ends either with the table of section headers (SHT) or with a section. */
+       sht_end = ehdr.e_shoff + (ehdr.e_shentsize * ehdr.e_shnum);
+       last_section_end = file64_to_cpu(shdr32.sh_offset) + file64_to_cpu(shdr32.sh_size);
+       return sht_end > last_section_end ? sht_end : last_section_end;
+}
+
+static off_t read_elf64(FILE* fd)
+{
+       Elf64_Ehdr ehdr64;
+       Elf64_Shdr shdr64;
+       off_t last_shdr_offset;
+       off_t ret;
+    off_t sht_end, last_section_end;
+
+       fseeko(fd, 0, SEEK_SET);
+       ret = fread(&ehdr64, 1, sizeof(ehdr64), fd);
+       if (ret < 0 || (size_t)ret != sizeof(ehdr64)) {
+               fprintf(stderr, "Read of ELF header from %s failed: %s\n",
+                       fname, strerror(errno));
+               return -1;
+       }
+
+       ehdr.e_shoff            = file64_to_cpu(ehdr64.e_shoff);
+       ehdr.e_shentsize        = file16_to_cpu(ehdr64.e_shentsize);
+       ehdr.e_shnum            = file16_to_cpu(ehdr64.e_shnum);
+
+       last_shdr_offset = ehdr.e_shoff + (ehdr.e_shentsize * (ehdr.e_shnum - 1));
+    fseeko(fd, last_shdr_offset, SEEK_SET);
+    ret = fread(&shdr64, 1, sizeof(shdr64), fd);
+       if (ret < 0 || ret != sizeof(shdr64)) {
+               fprintf(stderr, "Read of ELF section header from %s failed: %s\n",
+                       fname, strerror(errno));
+               return -1;
+       }
+
+       /* ELF ends either with the table of section headers (SHT) or with a section. */
+       sht_end = ehdr.e_shoff + (ehdr.e_shentsize * ehdr.e_shnum);
+       last_section_end = file64_to_cpu(shdr64.sh_offset) + file64_to_cpu(shdr64.sh_size);
+       return sht_end > last_section_end ? sht_end : last_section_end;
+}
+
+
+/* Return the offset, and the length of an ELF section with a given name in a given ELF file */
+bool appimage_get_elf_section_offset_and_length(const char* fname, const char* section_name, unsigned long* offset, unsigned long* length) {
+       uint8_t* data;
+       int i;
+       int fd = open(fname, O_RDONLY);
+       size_t map_size = (size_t) lseek(fd, 0, SEEK_END);
+
+       data = mmap(NULL, map_size, PROT_READ, MAP_SHARED, fd, 0);
+       close(fd);
+
+       // this trick works as both 32 and 64 bit ELF files start with the e_ident[EI_NINDENT] section
+       unsigned char class = data[EI_CLASS];
+
+       if (class == ELFCLASS32) {
+               Elf32_Ehdr* elf;
+               Elf32_Shdr* shdr;
+
+               elf = (Elf32_Ehdr*) data;
+               shdr = (Elf32_Shdr*) (data + ((Elf32_Ehdr*) elf)->e_shoff);
+
+               char* strTab = (char*) (data + shdr[elf->e_shstrndx].sh_offset);
+               for (i = 0; i < elf->e_shnum; i++) {
+                       if (strcmp(&strTab[shdr[i].sh_name], section_name) == 0) {
+                               *offset = shdr[i].sh_offset;
+                               *length = shdr[i].sh_size;
+                       }
+               }
+       } else if (class == ELFCLASS64) {
+               Elf64_Ehdr* elf;
+               Elf64_Shdr* shdr;
+
+               elf = (Elf64_Ehdr*) data;
+               shdr = (Elf64_Shdr*) (data + elf->e_shoff);
+
+               char* strTab = (char*) (data + shdr[elf->e_shstrndx].sh_offset);
+               for (i = 0; i < elf->e_shnum; i++) {
+                       if (strcmp(&strTab[shdr[i].sh_name], section_name) == 0) {
+                               *offset = shdr[i].sh_offset;
+                               *length = shdr[i].sh_size;
+                       }
+               }
+       } else {
+               fprintf(stderr, "Platforms other than 32-bit/64-bit are currently not supported!");
+               munmap(data, map_size);
+               return false;
+       }
+
+       munmap(data, map_size);
+       return true;
+}
+
+char* read_file_offset_length(const char* fname, unsigned long offset, unsigned long length) {
+       FILE* f;
+       if ((f = fopen(fname, "r")) == NULL) {
+               return NULL;
+       }
+
+       fseek(f, offset, SEEK_SET);
+
+       char* buffer = calloc(length + 1, sizeof(char));
+       fread(buffer, length, sizeof(char), f);
+
+       fclose(f);
+
+       return buffer;
+}
+
+int appimage_print_hex(const char* fname, unsigned long offset, unsigned long length) {
+       char* data;
+       if ((data = read_file_offset_length(fname, offset, length)) == NULL) {
+               return 1;
+       }
+
+       for (long long k = 0; k < length && data[k] != '\0'; k++) {
+               printf("%x", data[k]);
+       }
+
+       free(data);
+
+       printf("\n");
+
+       return 0;
+}
+
+int appimage_print_binary(const char* fname, unsigned long offset, unsigned long length) {
+       char* data;
+       if ((data = read_file_offset_length(fname, offset, length)) == NULL) {
+               return 1;
+       }
+
+       printf("%s\n", data);
+
+       free(data);
+
+       return 0;
+}
+
diff --git a/src/libappimage_shared/hexlify.c b/src/libappimage_shared/hexlify.c
new file mode 100644 (file)
index 0000000..113a595
--- /dev/null
@@ -0,0 +1,18 @@
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+char* appimage_hexlify(const char* bytes, const size_t numBytes) {
+    // first of all, allocate the new string
+    // a hexadecimal representation works like "every byte will be represented by two chars"
+    // additionally, we need to null-terminate the string
+    char* hexlified = (char*) calloc((2 * numBytes + 1), sizeof(char));
+
+    for (size_t i = 0; i < numBytes; i++) {
+        char buffer[3];
+        sprintf(buffer, "%02x", (unsigned char) bytes[i]);
+        strcat(hexlified, buffer);
+    }
+
+    return hexlified;
+}
diff --git a/src/libappimage_shared/light_byteswap.h b/src/libappimage_shared/light_byteswap.h
new file mode 100644 (file)
index 0000000..d0b98ba
--- /dev/null
@@ -0,0 +1,13 @@
+#pragma once
+
+#define bswap_16(value) \
+((((value) & 0xff) << 8) | ((value) >> 8))
+
+#define bswap_32(value) \
+(((uint32_t)bswap_16((uint16_t)((value) & 0xffff)) << 16) | \
+(uint32_t)bswap_16((uint16_t)((value) >> 16)))
+
+#define bswap_64(value) \
+(((uint64_t)bswap_32((uint32_t)((value) & 0xffffffff)) \
+<< 32) | \
+(uint64_t)bswap_32((uint32_t)((value) >> 32)))
diff --git a/src/libappimage_shared/light_elf.h b/src/libappimage_shared/light_elf.h
new file mode 100644 (file)
index 0000000..d6044a7
--- /dev/null
@@ -0,0 +1,119 @@
+/* 
+ * 
+ *  Linux kernel
+ *  Copyright (C) 2017 Linus Torvalds
+ *  Modified work Copyright (C) 2017 @teras (https://github.com/teras)
+ *  (Shortened version -- original work found here:
+ *   https://github.com/torvalds/linux/blob/master/include/uapi/linux/elf.h)
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License.
+ *
+ *  This program 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 General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ * 
+ */
+
+#ifndef LIGHT_ELF_H
+#define LIGHT_ELF_H
+
+#include <inttypes.h>
+
+__BEGIN_DECLS
+
+typedef uint16_t Elf32_Half;
+typedef uint16_t Elf64_Half;
+typedef uint32_t Elf32_Word;
+typedef uint32_t Elf64_Word;
+typedef uint64_t Elf64_Xword;
+typedef uint32_t Elf32_Addr;
+typedef uint64_t Elf64_Addr;
+typedef uint32_t Elf32_Off;
+typedef uint64_t Elf64_Off;
+
+#define EI_NIDENT 16
+
+typedef struct elf32_hdr {
+    unsigned char e_ident[EI_NIDENT];
+    Elf32_Half e_type;
+    Elf32_Half e_machine;
+    Elf32_Word e_version;
+    Elf32_Addr e_entry; /* Entry point */
+    Elf32_Off e_phoff;
+    Elf32_Off e_shoff;
+    Elf32_Word e_flags;
+    Elf32_Half e_ehsize;
+    Elf32_Half e_phentsize;
+    Elf32_Half e_phnum;
+    Elf32_Half e_shentsize;
+    Elf32_Half e_shnum;
+    Elf32_Half e_shstrndx;
+} Elf32_Ehdr;
+
+typedef struct elf64_hdr {
+    unsigned char e_ident[EI_NIDENT]; /* ELF "magic number" */
+    Elf64_Half e_type;
+    Elf64_Half e_machine;
+    Elf64_Word e_version;
+    Elf64_Addr e_entry; /* Entry point virtual address */
+    Elf64_Off e_phoff; /* Program header table file offset */
+    Elf64_Off e_shoff; /* Section header table file offset */
+    Elf64_Word e_flags;
+    Elf64_Half e_ehsize;
+    Elf64_Half e_phentsize;
+    Elf64_Half e_phnum;
+    Elf64_Half e_shentsize;
+    Elf64_Half e_shnum;
+    Elf64_Half e_shstrndx;
+} Elf64_Ehdr;
+
+typedef struct elf32_shdr {
+    Elf32_Word sh_name;
+    Elf32_Word sh_type;
+    Elf32_Word sh_flags;
+    Elf32_Addr sh_addr;
+    Elf32_Off sh_offset;
+    Elf32_Word sh_size;
+    Elf32_Word sh_link;
+    Elf32_Word sh_info;
+    Elf32_Word sh_addralign;
+    Elf32_Word sh_entsize;
+} Elf32_Shdr;
+
+typedef struct elf64_shdr {
+    Elf64_Word sh_name; /* Section name, index in string tbl */
+    Elf64_Word sh_type; /* Type of section */
+    Elf64_Xword sh_flags; /* Miscellaneous section attributes */
+    Elf64_Addr sh_addr; /* Section virtual addr at execution */
+    Elf64_Off sh_offset; /* Section file offset */
+    Elf64_Xword sh_size; /* Size of section in bytes */
+    Elf64_Word sh_link; /* Index of another section */
+    Elf64_Word sh_info; /* Additional section information */
+    Elf64_Xword sh_addralign; /* Section alignment */
+    Elf64_Xword sh_entsize; /* Entry size if section holds table */
+} Elf64_Shdr;
+
+/* Note header in a PT_NOTE section */
+typedef struct elf32_note {
+    Elf32_Word n_namesz; /* Name size */
+    Elf32_Word n_descsz; /* Content size */
+    Elf32_Word n_type; /* Content type */
+} Elf32_Nhdr;
+
+#define ELFCLASS32  1
+#define ELFDATA2LSB 1
+#define ELFDATA2MSB 2
+#define ELFCLASS64  2
+#define EI_CLASS    4
+#define EI_DATA     5
+
+__END_DECLS
+
+#endif /* elf.h */
diff --git a/src/patches/patch-squashfuse.sh.in b/src/patches/patch-squashfuse.sh.in
new file mode 100755 (executable)
index 0000000..bd3452f
--- /dev/null
@@ -0,0 +1,8 @@
+#! /bin/bash
+
+git checkout ll.c Makefile.am fuseprivate.c fuseprivate.h hl.c ll.h ll_inode.c nonstd-daemon.c
+
+patch -p1 < @PROJECT_SOURCE_DIR@/src/patches/squashfuse.patch
+patch -p1 < @PROJECT_SOURCE_DIR@/src/patches/squashfuse_dlopen.patch
+
+cp -v @PROJECT_SOURCE_DIR@/src/patches/squashfuse_dlopen.c @PROJECT_SOURCE_DIR@/src/patches/squashfuse_dlopen.h .
diff --git a/src/patches/squashfuse.patch b/src/patches/squashfuse.patch
new file mode 100644 (file)
index 0000000..db94d2f
--- /dev/null
@@ -0,0 +1,57 @@
+diff --git a/Makefile.am b/Makefile.am
+index f0d7cde..70c4aa0 100644
+--- a/Makefile.am
++++ b/Makefile.am
+@@ -14,6 +14,7 @@ bin_PROGRAMS =
+ noinst_PROGRAMS =
+ noinst_LTLIBRARIES = libsquashfuse.la
++noinst_LTLIBRARIES += libsquashfuse_ll.la
+ # Main library: libsquashfuse
+ libsquashfuse_la_SOURCES = swap.c cache.c table.c dir.c file.c fs.c \
+@@ -46,10 +47,9 @@ endif
+ # Low-level squashfuse_ll, if supported
+ if SQ_WANT_LOWLEVEL
+-bin_PROGRAMS += squashfuse_ll
+-squashfuse_ll_SOURCES = ll.c ll_inode.c nonstd-daemon.c ll.h
+-squashfuse_ll_CPPFLAGS = $(FUSE_CPPFLAGS)
+-squashfuse_ll_LDADD = libsquashfuse.la libfuseprivate.la $(COMPRESSION_LIBS) \
++libsquashfuse_ll_la_SOURCES = ll.c ll_inode.c nonstd-daemon.c ll.h
++libsquashfuse_ll_la_CPPFLAGS = $(FUSE_CPPFLAGS)
++libsquashfuse_ll_la_LIBADD = libsquashfuse.la libfuseprivate.la $(COMPRESSION_LIBS) \
+   $(FUSE_LIBS)
+ noinst_LTLIBRARIES += libfuseprivate.la
+diff --git a/ll.c b/ll.c
+index a2c7902..8fcb3f4 100644
+--- a/ll.c
++++ b/ll.c
+@@ -390,7 +390,7 @@ static sqfs_ll *sqfs_ll_open(const char *path, size_t offset) {
+       return NULL;
+ }
+-int main(int argc, char *argv[]) {
++int fusefs_main(int argc, char *argv[], void (*mounted) (void)) {
+       struct fuse_args args;
+       sqfs_opts opts;
+       
+@@ -451,6 +451,8 @@ int main(int argc, char *argv[]) {
+                               if (sqfs_ll_daemonize(fg) != -1) {
+                                       if (fuse_set_signal_handlers(se) != -1) {
+                                               fuse_session_add_chan(se, ch.ch);
++                              if (mounted)
++                                mounted ();
+                                               /* FIXME: multithreading */
+                                               err = fuse_session_loop(se);
+                                               fuse_remove_signal_handlers(se);
+@@ -466,6 +468,8 @@ int main(int argc, char *argv[]) {
+               }
+       }
+       fuse_opt_free_args(&args);
++      if (mounted)
++        rmdir (mountpoint);
+       free(ll);
+       free(mountpoint);
+       
diff --git a/src/patches/squashfuse_dlopen.c b/src/patches/squashfuse_dlopen.c
new file mode 100644 (file)
index 0000000..fec4b76
--- /dev/null
@@ -0,0 +1,12 @@
+#include "squashfuse_dlopen.h"
+
+void *libhandle = NULL;
+int have_libloaded = 0;
+
+const char *load_library_errmsg =
+  "AppImages require FUSE to run. \n"
+  "You might still be able to extract the contents of this AppImage \n"
+  "if you run it with the --appimage-extract option. \n"
+  "See https://github.com/AppImage/AppImageKit/wiki/FUSE \n"
+  "for more information\n";
+
diff --git a/src/patches/squashfuse_dlopen.h b/src/patches/squashfuse_dlopen.h
new file mode 100644 (file)
index 0000000..1b93a8e
--- /dev/null
@@ -0,0 +1,262 @@
+#ifndef SQFS_DLOPEN_H
+#define SQFS_DLOPEN_H
+
+//#define ENABLE_DLOPEN
+
+#ifdef ENABLE_DLOPEN
+
+#include <dlfcn.h>
+
+#include <time.h>
+#include <utime.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/statvfs.h>
+#include <sys/uio.h>
+
+
+/*** dlopen() stuff ***/
+
+#define LIBNAME "libfuse.so.2"
+
+extern void *libhandle;
+extern int have_libloaded;
+extern const char *load_library_errmsg;
+
+#define LOAD_LIBRARY \
+if (have_libloaded != 1) { \
+  if (!(libhandle = dlopen(LIBNAME, RTLD_LAZY))) { \
+    fprintf(stderr, "dlopen(): error loading " LIBNAME "\n\n%s", load_library_errmsg ); \
+    exit(1); \
+  } else { \
+    have_libloaded = 1; \
+  } \
+}
+
+#define STRINGIFY(x) #x
+
+#define LOAD_SYMBOL(type,x,param) \
+type (*dl_##x) param; \
+*(void **) (&dl_##x) = dlsym(libhandle, STRINGIFY(x)); \
+if (dlerror()) { \
+  fprintf(stderr, "dlsym(): error loading symbol from " LIBNAME "\n\n%s", load_library_errmsg ); \
+  CLOSE_LIBRARY; \
+  exit(1); \
+}
+
+#define DL(x) dl_##x
+#define CLOSE_LIBRARY dlclose(libhandle);
+
+
+/*** libfuse stuff ***/
+
+#define FUSE_ROOT_ID 1
+#define FUSE_ARGS_INIT(argc, argv) { argc, argv, 0 }
+#define FUSE_OPT_KEY(templ, key) { templ, -1U, key }
+#define FUSE_OPT_KEY_OPT -1
+#define FUSE_OPT_KEY_NONOPT -2
+#define FUSE_OPT_END { NULL, 0, 0 }
+
+enum fuse_buf_flags {
+  FUSE_BUF_IS_FD               = (1 << 1),
+  FUSE_BUF_FD_SEEK     = (1 << 2),
+  FUSE_BUF_FD_RETRY    = (1 << 3),
+};
+
+typedef unsigned long fuse_ino_t;
+typedef struct fuse_req *fuse_req_t;
+
+struct fuse_chan;
+struct fuse_pollhandle;
+
+struct fuse_args {
+  int argc;
+  char **argv;
+  int allocated;
+};
+
+typedef int (*fuse_fill_dir_t) (void *buf, const char *name, const struct stat *stbuf, off_t off);
+typedef int (*fuse_opt_proc_t)(void *data, const char *arg, int key, struct fuse_args *outargs);
+typedef struct fuse_dirhandle *fuse_dirh_t;
+typedef int (*fuse_dirfil_t) (fuse_dirh_t h, const char *name, int type, ino_t ino);
+
+struct fuse_file_info {
+  int flags;
+  unsigned long fh_old;
+  int writepage;
+  unsigned int direct_io : 1;
+  unsigned int keep_cache : 1;
+  unsigned int flush : 1;
+  unsigned int nonseekable : 1;
+  unsigned int flock_release : 1;
+  unsigned int padding : 27;
+  uint64_t fh;
+  uint64_t lock_owner;
+};
+
+struct fuse_entry_param {
+  fuse_ino_t ino;
+  unsigned long generation;
+  struct stat attr;
+  double attr_timeout;
+  double entry_timeout;
+};
+
+struct fuse_opt {
+  const char *templ;
+  unsigned long offset;
+  int value;
+};
+
+struct fuse_forget_data {
+  uint64_t ino;
+  uint64_t nlookup;
+};
+
+struct fuse_conn_info {
+  unsigned proto_major;
+  unsigned proto_minor;
+  unsigned async_read;
+  unsigned max_write;
+  unsigned max_readahead;
+  unsigned capable;
+  unsigned want;
+  unsigned max_background;
+  unsigned congestion_threshold;
+  unsigned reserved[23];
+};
+
+struct fuse_buf {
+  size_t size;
+  enum fuse_buf_flags flags;
+  void *mem;
+  int fd;
+  off_t pos;
+};
+
+struct fuse_bufvec {
+  size_t count;
+  size_t idx;
+  size_t off;
+  struct fuse_buf buf[1];
+};
+
+struct fuse_context {
+  struct fuse *fuse;
+  uid_t uid;
+  gid_t gid;
+  pid_t pid;
+  void *private_data;
+  mode_t umask;
+};
+
+struct fuse_operations {
+       int (*getattr) (const char *, struct stat *);
+       int (*readlink) (const char *, char *, size_t);
+       int (*getdir) (const char *, fuse_dirh_t, fuse_dirfil_t);
+       int (*mknod) (const char *, mode_t, dev_t);
+       int (*mkdir) (const char *, mode_t);
+       int (*unlink) (const char *);
+       int (*rmdir) (const char *);
+       int (*symlink) (const char *, const char *);
+       int (*rename) (const char *, const char *);
+       int (*link) (const char *, const char *);
+       int (*chmod) (const char *, mode_t);
+       int (*chown) (const char *, uid_t, gid_t);
+       int (*truncate) (const char *, off_t);
+       int (*utime) (const char *, struct utimbuf *);
+       int (*open) (const char *, struct fuse_file_info *);
+       int (*read) (const char *, char *, size_t, off_t, struct fuse_file_info *);
+       int (*write) (const char *, const char *, size_t, off_t, struct fuse_file_info *);
+       int (*statfs) (const char *, struct statvfs *);
+       int (*flush) (const char *, struct fuse_file_info *);
+       int (*release) (const char *, struct fuse_file_info *);
+       int (*fsync) (const char *, int, struct fuse_file_info *);
+       int (*setxattr) (const char *, const char *, const char *, size_t, int);
+       int (*getxattr) (const char *, const char *, char *, size_t);
+       int (*listxattr) (const char *, char *, size_t);
+       int (*removexattr) (const char *, const char *);
+       int (*opendir) (const char *, struct fuse_file_info *);
+       int (*readdir) (const char *, void *, fuse_fill_dir_t, off_t, struct fuse_file_info *);
+       int (*releasedir) (const char *, struct fuse_file_info *);
+       int (*fsyncdir) (const char *, int, struct fuse_file_info *);
+       void *(*init) (struct fuse_conn_info *conn);
+       void (*destroy) (void *);
+       int (*access) (const char *, int);
+       int (*create) (const char *, mode_t, struct fuse_file_info *);
+       int (*ftruncate) (const char *, off_t, struct fuse_file_info *);
+       int (*fgetattr) (const char *, struct stat *, struct fuse_file_info *);
+       int (*lock) (const char *, struct fuse_file_info *, int cmd, struct flock *);
+       int (*utimens) (const char *, const struct timespec tv[2]);
+       int (*bmap) (const char *, size_t blocksize, uint64_t *idx);
+       unsigned int flag_nullpath_ok:1;
+       unsigned int flag_nopath:1;
+       unsigned int flag_utime_omit_ok:1;
+       unsigned int flag_reserved:29;
+       int (*ioctl) (const char *, int cmd, void *arg, struct fuse_file_info *, unsigned int flags, void *data);
+       int (*poll) (const char *, struct fuse_file_info *, struct fuse_pollhandle *ph, unsigned *reventsp);
+       int (*write_buf) (const char *, struct fuse_bufvec *buf, off_t off, struct fuse_file_info *);
+       int (*read_buf) (const char *, struct fuse_bufvec **bufp, size_t size, off_t off, struct fuse_file_info *);
+       int (*flock) (const char *, struct fuse_file_info *, int op);
+       int (*fallocate) (const char *, int, off_t, off_t, struct fuse_file_info *);
+};
+
+struct fuse_lowlevel_ops {
+  void (*init) (void *userdata, struct fuse_conn_info *conn);
+  void (*destroy) (void *userdata);
+  void (*lookup) (fuse_req_t req, fuse_ino_t parent, const char *name);
+  void (*forget) (fuse_req_t req, fuse_ino_t ino, unsigned long nlookup);
+  void (*getattr) (fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi);
+  void (*setattr) (fuse_req_t req, fuse_ino_t ino, struct stat *attr, int to_set, struct fuse_file_info *fi);
+  void (*readlink) (fuse_req_t req, fuse_ino_t ino);
+  void (*mknod) (fuse_req_t req, fuse_ino_t parent, const char *name, mode_t mode, dev_t rdev);
+  void (*mkdir) (fuse_req_t req, fuse_ino_t parent, const char *name, mode_t mode);
+  void (*unlink) (fuse_req_t req, fuse_ino_t parent, const char *name);
+  void (*rmdir) (fuse_req_t req, fuse_ino_t parent, const char *name);
+  void (*symlink) (fuse_req_t req, const char *link, fuse_ino_t parent, const char *name);
+  void (*rename) (fuse_req_t req, fuse_ino_t parent, const char *name, fuse_ino_t newparent, const char *newname);
+  void (*link) (fuse_req_t req, fuse_ino_t ino, fuse_ino_t newparent, const char *newname);
+  void (*open) (fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi);
+  void (*read) (fuse_req_t req, fuse_ino_t ino, size_t size, off_t off, struct fuse_file_info *fi);
+  void (*write) (fuse_req_t req, fuse_ino_t ino, const char *buf, size_t size, off_t off, struct fuse_file_info *fi);
+  void (*flush) (fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi);
+  void (*release) (fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi);
+  void (*fsync) (fuse_req_t req, fuse_ino_t ino, int datasync, struct fuse_file_info *fi);
+  void (*opendir) (fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi);
+  void (*readdir) (fuse_req_t req, fuse_ino_t ino, size_t size, off_t off, struct fuse_file_info *fi);
+  void (*releasedir) (fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi);
+  void (*fsyncdir) (fuse_req_t req, fuse_ino_t ino, int datasync, struct fuse_file_info *fi);
+  void (*statfs) (fuse_req_t req, fuse_ino_t ino);
+  void (*setxattr) (fuse_req_t req, fuse_ino_t ino, const char *name, const char *value, size_t size, int flags);
+  void (*getxattr) (fuse_req_t req, fuse_ino_t ino, const char *name, size_t size);
+  void (*listxattr) (fuse_req_t req, fuse_ino_t ino, size_t size);
+  void (*removexattr) (fuse_req_t req, fuse_ino_t ino, const char *name);
+  void (*access) (fuse_req_t req, fuse_ino_t ino, int mask);
+  void (*create) (fuse_req_t req, fuse_ino_t parent, const char *name, mode_t mode, struct fuse_file_info *fi);
+  void (*getlk) (fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi, struct flock *lock);
+  void (*setlk) (fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi, struct flock *lock, int sleep);
+  void (*bmap) (fuse_req_t req, fuse_ino_t ino, size_t blocksize, uint64_t idx);
+  void (*ioctl) (fuse_req_t req, fuse_ino_t ino, int cmd, void *arg, struct fuse_file_info *fi, unsigned flags, const void *in_buf, size_t in_bufsz, size_t out_bufsz);
+  void (*poll) (fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi, struct fuse_pollhandle *ph);
+  void (*write_buf) (fuse_req_t req, fuse_ino_t ino, struct fuse_bufvec *bufv, off_t off, struct fuse_file_info *fi);
+  void (*retrieve_reply) (fuse_req_t req, void *cookie, fuse_ino_t ino, off_t offset, struct fuse_bufvec *bufv);
+  void (*forget_multi) (fuse_req_t req, size_t count, struct fuse_forget_data *forgets);
+  void (*flock) (fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi, int op);
+  void (*fallocate) (fuse_req_t req, fuse_ino_t ino, int mode, off_t offset, off_t length, struct fuse_file_info *fi);
+};
+
+#else  /* !ENABLE_DLOPEN */
+
+#define LOAD_LIBRARY
+#define LOAD_SYMBOL(x)
+#define DL(x)
+#define CLOSE_LIBRARY
+
+#endif  /* !ENABLE_DLOPEN */
+
+#endif /* SQFS_DLOPEN_H */
+
diff --git a/src/patches/squashfuse_dlopen.patch b/src/patches/squashfuse_dlopen.patch
new file mode 100644 (file)
index 0000000..59e1e6e
--- /dev/null
@@ -0,0 +1,640 @@
+--- a/Makefile.am
++++ b/Makefile.am
+@@ -1,6 +1,7 @@
+ COMPRESSION_LIBS = $(ZLIB_LIBS) $(XZ_LIBS) $(LZO_LIBS) $(LZ4_LIBS)
+ ACLOCAL_AMFLAGS = -I m4 --install
++AM_CFLAGS = -fno-strict-aliasing -DENABLE_DLOPEN
+ # Suppress AppleDouble
+ if MAKE_EXPORT
+@@ -19,13 +20,13 @@
+ # Main library: libsquashfuse
+ libsquashfuse_la_SOURCES = swap.c cache.c table.c dir.c file.c fs.c \
+       decompress.c xattr.c hash.c stack.c traverse.c util.c \
+-      nonstd-pread.c nonstd-stat.c \
++      nonstd-pread.c nonstd-stat.c squashfuse_dlopen.c \
+       squashfs_fs.h common.h nonstd-internal.h nonstd.h swap.h cache.h table.h \
+       dir.h file.h decompress.h xattr.h squashfuse.h hash.h stack.h traverse.h \
+-      util.h fs.h
++      util.h fs.h squashfuse_dlopen.h
+ libsquashfuse_la_CPPFLAGS = $(ZLIB_CPPFLAGS) $(XZ_CPPFLAGS) $(LZO_CPPFLAGS) \
+       $(LZ4_CPPFLAGS)
+-libsquashfuse_la_LIBADD =
++libsquashfuse_la_LIBADD = -ldl
+ # Helper for FUSE clients: libfuseprivate
+ libfuseprivate_la_SOURCES = fuseprivate.c nonstd-makedev.c nonstd-enoattr.c \
+--- a/fuseprivate.c
++++ b/fuseprivate.c
+@@ -94,15 +94,17 @@
+ }
+ void sqfs_usage(char *progname, bool fuse_usage) {
++      LOAD_SYMBOL(int,fuse_opt_add_arg,(struct fuse_args *args, const char *arg));
++      LOAD_SYMBOL(int,fuse_parse_cmdline,(struct fuse_args *args, char **mountpoint, int *multithreaded, int *foreground));
+       fprintf(stderr, "%s (c) 2012 Dave Vasilevsky\n\n", PACKAGE_STRING);
+       fprintf(stderr, "Usage: %s [options] ARCHIVE MOUNTPOINT\n",
+               progname ? progname : PACKAGE_NAME);
+       if (fuse_usage) {
+               struct fuse_args args = FUSE_ARGS_INIT(0, NULL);
+-              fuse_opt_add_arg(&args, ""); /* progname */
+-              fuse_opt_add_arg(&args, "-ho");
++              DL(fuse_opt_add_arg)(&args, ""); /* progname */
++              DL(fuse_opt_add_arg)(&args, "-ho");
+               fprintf(stderr, "\n");
+-              fuse_parse_cmdline(&args, NULL, NULL, NULL);
++              DL(fuse_parse_cmdline)(&args, NULL, NULL, NULL);
+       }
+       exit(-2);
+ }
+--- a/fuseprivate.h
++++ b/fuseprivate.h
+@@ -27,7 +27,10 @@
+ #include "squashfuse.h"
+-#include <fuse.h>
++#include "squashfuse_dlopen.h"
++#ifndef ENABLE_DLOPEN
++#     include <fuse.h>
++#endif
+ #include <sys/stat.h>
+--- a/hl.c
++++ b/hl.c
+@@ -33,6 +33,7 @@
+ #include <stdlib.h>
+ #include <string.h>
++int have_libloaded = 0;
+ typedef struct sqfs_hl sqfs_hl;
+ struct sqfs_hl {
+@@ -42,9 +43,10 @@
+ static sqfs_err sqfs_hl_lookup(sqfs **fs, sqfs_inode *inode,
+               const char *path) {
++      LOAD_SYMBOL(struct fuse_context *,fuse_get_context,(void));
+       bool found;
+       
+-      sqfs_hl *hl = fuse_get_context()->private_data;
++      sqfs_hl *hl = DL(fuse_get_context)()->private_data;
+       *fs = &hl->fs;
+       if (inode)
+               *inode = hl->root; /* copy */
+@@ -67,7 +69,8 @@
+ }
+ static void *sqfs_hl_op_init(struct fuse_conn_info *conn) {
+-      return fuse_get_context()->private_data;
++      LOAD_SYMBOL(struct fuse_context *,fuse_get_context,(void));
++      return DL(fuse_get_context)()->private_data;
+ }
+ static int sqfs_hl_op_getattr(const char *path, struct stat *st) {
+@@ -264,7 +267,16 @@
+       return NULL;
+ }
++#ifdef ENABLE_DLOPEN
++#define fuse_main(argc, argv, op, user_data) \
++      DL(fuse_main_real)(argc, argv, op, sizeof(*(op)), user_data)
++#endif
++
+ int main(int argc, char *argv[]) {
++      LOAD_SYMBOL(int,fuse_opt_parse,(struct fuse_args *args, void *data, const struct fuse_opt opts[], fuse_opt_proc_t proc));
++      LOAD_SYMBOL(int,fuse_opt_add_arg,(struct fuse_args *args, const char *arg));
++      LOAD_SYMBOL(int,fuse_main_real,(int argc, char *argv[], const struct fuse_operations *op, size_t op_size, void *user_data));  /* fuse_main */
++      LOAD_SYMBOL(void,fuse_opt_free_args,(struct fuse_args *args));
+       struct fuse_args args;
+       sqfs_opts opts;
+       sqfs_hl *hl;
+@@ -299,7 +311,7 @@
+       opts.image = NULL;
+       opts.mountpoint = 0;
+       opts.offset = 0;
+-      if (fuse_opt_parse(&args, &opts, fuse_opts, sqfs_opt_proc) == -1)
++      if (DL(fuse_opt_parse)(&args, &opts, fuse_opts, sqfs_opt_proc) == -1)
+               sqfs_usage(argv[0], true);
+       if (!opts.image)
+               sqfs_usage(argv[0], true);
+@@ -308,8 +320,9 @@
+       if (!hl)
+               return -1;
+       
+-      fuse_opt_add_arg(&args, "-s"); /* single threaded */
++      DL(fuse_opt_add_arg)(&args, "-s"); /* single threaded */
+       ret = fuse_main(args.argc, args.argv, &sqfs_hl_ops, hl);
+-      fuse_opt_free_args(&args);
++      DL(fuse_opt_free_args)(&args);
++      CLOSE_LIBRARY;
+       return ret;
+ }
+--- a/ll.h
++++ b/ll.h
+@@ -27,7 +27,10 @@
+ #include "squashfuse.h"
+-#include <fuse_lowlevel.h>
++#include "squashfuse_dlopen.h"
++#ifndef ENABLE_DLOPEN
++#     include <fuse_lowlevel.h>
++#endif
+ typedef struct sqfs_ll sqfs_ll;
+ struct sqfs_ll {
+--- a/ll_inode.c
++++ b/ll_inode.c
+@@ -348,12 +348,14 @@
+ sqfs_err sqfs_ll_iget(fuse_req_t req, sqfs_ll_i *lli, fuse_ino_t i) {
++      LOAD_SYMBOL(void *,fuse_req_userdata,(fuse_req_t req));
++      LOAD_SYMBOL(int,fuse_reply_err,(fuse_req_t req, int err));
+       sqfs_err err = SQFS_OK;
+-      lli->ll = fuse_req_userdata(req);
++      lli->ll = DL(fuse_req_userdata)(req);
+       if (i != SQFS_FUSE_INODE_NONE) {
+               err = sqfs_ll_inode(lli->ll, &lli->inode, i);
+               if (err)
+-                      fuse_reply_err(req, ENOENT);
++                      DL(fuse_reply_err)(req, ENOENT);
+       }
+       return err;
+ }
+--- a/nonstd-daemon.c
++++ b/nonstd-daemon.c
+@@ -28,11 +28,16 @@
+ #include "nonstd-internal.h"
+ #include <unistd.h>
+-#include <fuse_lowlevel.h>
++
++#include "squashfuse_dlopen.h"
++#ifndef ENABLE_DLOPEN
++#     include <fuse_lowlevel.h>
++#endif
+ int sqfs_ll_daemonize(int fg) {
+       #if HAVE_DECL_FUSE_DAEMONIZE
+-              return fuse_daemonize(fg);
++              LOAD_SYMBOL(int,fuse_daemonize,(int foreground));
++              return DL(fuse_daemonize)(fg);
+       #else
+               return daemon(0,0);
+       #endif
+--- a/ll.c
++++ b/ll.c
+@@ -38,37 +38,41 @@
+ static void sqfs_ll_op_getattr(fuse_req_t req, fuse_ino_t ino,
+               struct fuse_file_info *fi) {
++      LOAD_SYMBOL(int,fuse_reply_err,(fuse_req_t req, int err));
++      LOAD_SYMBOL(int,fuse_reply_attr,(fuse_req_t req, const struct stat *attr, double attr_timeout));
+       sqfs_ll_i lli;
+       struct stat st;
+       if (sqfs_ll_iget(req, &lli, ino))
+               return;
+       
+       if (sqfs_stat(&lli.ll->fs, &lli.inode, &st)) {
+-              fuse_reply_err(req, ENOENT);
++              DL(fuse_reply_err)(req, ENOENT);
+       } else {
+               st.st_ino = ino;
+-              fuse_reply_attr(req, &st, SQFS_TIMEOUT);
++              DL(fuse_reply_attr)(req, &st, SQFS_TIMEOUT);
+       }
+ }
+ static void sqfs_ll_op_opendir(fuse_req_t req, fuse_ino_t ino,
+               struct fuse_file_info *fi) {
++      LOAD_SYMBOL(int,fuse_reply_err,(fuse_req_t req, int err));
++      LOAD_SYMBOL(int,fuse_reply_open,(fuse_req_t req, const struct fuse_file_info *fi));
+       sqfs_ll_i *lli;
+       
+       fi->fh = (intptr_t)NULL;
+       
+       lli = malloc(sizeof(*lli));
+       if (!lli) {
+-              fuse_reply_err(req, ENOMEM);
++              DL(fuse_reply_err)(req, ENOMEM);
+               return;
+       }
+       
+       if (sqfs_ll_iget(req, lli, ino) == SQFS_OK) {
+               if (!S_ISDIR(lli->inode.base.mode)) {
+-                      fuse_reply_err(req, ENOTDIR);
++                      DL(fuse_reply_err)(req, ENOTDIR);
+               } else {
+                       fi->fh = (intptr_t)lli;
+-                      fuse_reply_open(req, fi);
++                      DL(fuse_reply_open)(req, fi);
+                       return;
+               }
+       }
+@@ -77,28 +81,35 @@
+ static void sqfs_ll_op_create(fuse_req_t req, fuse_ino_t parent, const char *name,
+                             mode_t mode, struct fuse_file_info *fi) {
+-      fuse_reply_err(req, EROFS);
++      LOAD_SYMBOL(int,fuse_reply_err,(fuse_req_t req, int err));
++      DL(fuse_reply_err)(req, EROFS);
+ }
+ static void sqfs_ll_op_releasedir(fuse_req_t req, fuse_ino_t ino,
+               struct fuse_file_info *fi) {
+       free((sqfs_ll_i*)(intptr_t)fi->fh);
+-      fuse_reply_err(req, 0); /* yes, this is necessary */
++      LOAD_SYMBOL(int,fuse_reply_err,(fuse_req_t req, int err));
++      DL(fuse_reply_err)(req, 0); /* yes, this is necessary */
+ }
+ static size_t sqfs_ll_add_direntry(fuse_req_t req, char *buf, size_t bufsize,
+               const char *name, const struct stat *st, off_t off) {
+       #if HAVE_DECL_FUSE_ADD_DIRENTRY
+-              return fuse_add_direntry(req, buf, bufsize, name, st, off);
++              LOAD_SYMBOL(size_t,fuse_add_direntry,(fuse_req_t req, char *buf, size_t bufsize, const char *name, const struct stat *stbuf, off_t off));
++              return DL(fuse_add_direntry)(req, buf, bufsize, name, st, off);
+       #else
+-              size_t esize = fuse_dirent_size(strlen(name));
++              LOAD_SYMBOL(size_t,fuse_dirent_size(size_t namelen));
++              LOAD_SYMBOL(char *,fuse_add_dirent,(char *buf, const char *name, const struct stat *stbuf, off_t off));
++              size_t esize = DL(fuse_dirent_size)(strlen(name));
+               if (bufsize >= esize)
+-                      fuse_add_dirent(buf, name, st, off);
++                      DL(fuse_add_dirent)(buf, name, st, off);
+               return esize;
+       #endif
+ }
+ static void sqfs_ll_op_readdir(fuse_req_t req, fuse_ino_t ino, size_t size,
+               off_t off, struct fuse_file_info *fi) {
++      LOAD_SYMBOL(int,fuse_reply_err,(fuse_req_t req, int err));
++      LOAD_SYMBOL(int,fuse_reply_buf,(fuse_req_t req, const char *buf, size_t size));
+       sqfs_err sqerr;
+       sqfs_dir dir;
+       sqfs_name namebuf;
+@@ -135,14 +146,16 @@
+       }
+       
+       if (err)
+-              fuse_reply_err(req, err);
++              DL(fuse_reply_err)(req, err);
+       else
+-              fuse_reply_buf(req, buf, bufpos - buf);
++              DL(fuse_reply_buf)(req, buf, bufpos - buf);
+       free(buf);
+ }
+ static void sqfs_ll_op_lookup(fuse_req_t req, fuse_ino_t parent,
+               const char *name) {
++      LOAD_SYMBOL(int,fuse_reply_err,(fuse_req_t req, int err));
++      LOAD_SYMBOL(int,fuse_reply_entry,(fuse_req_t req, const struct fuse_entry_param *e));
+       sqfs_ll_i lli;
+       sqfs_err sqerr;
+       sqfs_name namebuf;
+@@ -154,7 +167,7 @@
+               return;
+       
+       if (!S_ISDIR(lli.inode.base.mode)) {
+-              fuse_reply_err(req, ENOTDIR);
++              DL(fuse_reply_err)(req, ENOTDIR);
+               return;
+       }
+       
+@@ -162,55 +175,58 @@
+       sqerr = sqfs_dir_lookup(&lli.ll->fs, &lli.inode, name, strlen(name), &entry,
+               &found);
+       if (sqerr) {
+-              fuse_reply_err(req, EIO);
++              DL(fuse_reply_err)(req, EIO);
+               return;
+       }
+       if (!found) {
+-              fuse_reply_err(req, ENOENT);
++              DL(fuse_reply_err)(req, ENOENT);
+               return;
+       }
+       
+       if (sqfs_inode_get(&lli.ll->fs, &inode, sqfs_dentry_inode(&entry))) {
+-              fuse_reply_err(req, ENOENT);
++              DL(fuse_reply_err)(req, ENOENT);
+       } else {
+               struct fuse_entry_param fentry;
+               memset(&fentry, 0, sizeof(fentry));
+               if (sqfs_stat(&lli.ll->fs, &inode, &fentry.attr)) {
+-                      fuse_reply_err(req, EIO);
++                      DL(fuse_reply_err)(req, EIO);
+               } else {
+                       fentry.attr_timeout = fentry.entry_timeout = SQFS_TIMEOUT;
+                       fentry.ino = lli.ll->ino_register(lli.ll, &entry);
+                       fentry.attr.st_ino = fentry.ino;
+-                      fuse_reply_entry(req, &fentry);
++                      DL(fuse_reply_entry)(req, &fentry);
+               }
+       }
+ }
+ static void sqfs_ll_op_open(fuse_req_t req, fuse_ino_t ino,
+               struct fuse_file_info *fi) {
++      LOAD_SYMBOL(int,fuse_reply_err,(fuse_req_t req, int err));
++      LOAD_SYMBOL(int,fuse_reply_open,(fuse_req_t req, const struct fuse_file_info *fi));
++      LOAD_SYMBOL(void *,fuse_req_userdata,(fuse_req_t req));
+       sqfs_inode *inode;
+       sqfs_ll *ll;
+       
+       if (fi->flags & (O_WRONLY | O_RDWR)) {
+-              fuse_reply_err(req, EROFS);
++              DL(fuse_reply_err)(req, EROFS);
+               return;
+       }
+       
+       inode = malloc(sizeof(sqfs_inode));
+       if (!inode) {
+-              fuse_reply_err(req, ENOMEM);
++              DL(fuse_reply_err)(req, ENOMEM);
+               return;
+       }
+       
+-      ll = fuse_req_userdata(req);
++      ll = DL(fuse_req_userdata)(req);
+       if (sqfs_ll_inode(ll, inode, ino)) {
+-              fuse_reply_err(req, ENOENT);
++              DL(fuse_reply_err)(req, ENOENT);
+       } else if (!S_ISREG(inode->base.mode)) {
+-              fuse_reply_err(req, EISDIR);
++              DL(fuse_reply_err)(req, EISDIR);
+       } else {
+               fi->fh = (intptr_t)inode;
+               fi->keep_cache = 1;
+-              fuse_reply_open(req, fi);
++              DL(fuse_reply_open)(req, fi);
+               return;
+       }
+       free(inode);
+@@ -218,37 +234,43 @@
+ static void sqfs_ll_op_release(fuse_req_t req, fuse_ino_t ino,
+               struct fuse_file_info *fi) {
++      LOAD_SYMBOL(int,fuse_reply_err,(fuse_req_t req, int err));
+       free((sqfs_inode*)(intptr_t)fi->fh);
+       fi->fh = 0;
+-      fuse_reply_err(req, 0);
++      DL(fuse_reply_err)(req, 0);
+ }
+ static void sqfs_ll_op_read(fuse_req_t req, fuse_ino_t ino,
+               size_t size, off_t off, struct fuse_file_info *fi) {
+-      sqfs_ll *ll = fuse_req_userdata(req);
++      LOAD_SYMBOL(void *,fuse_req_userdata,(fuse_req_t req));
++      LOAD_SYMBOL(int,fuse_reply_err,(fuse_req_t req, int err));
++      LOAD_SYMBOL(int,fuse_reply_buf,(fuse_req_t req, const char *buf, size_t size));
++      sqfs_ll *ll = DL(fuse_req_userdata)(req);
+       sqfs_inode *inode = (sqfs_inode*)(intptr_t)fi->fh;
+       sqfs_err err = SQFS_OK;
+       
+       off_t osize;
+       char *buf = malloc(size);
+       if (!buf) {
+-              fuse_reply_err(req, ENOMEM);
++              DL(fuse_reply_err)(req, ENOMEM);
+               return;
+       }
+       
+       osize = size;
+       err = sqfs_read_range(&ll->fs, inode, off, &osize, buf);
+       if (err) {
+-              fuse_reply_err(req, EIO);
++              DL(fuse_reply_err)(req, EIO);
+       } else if (osize == 0) { /* EOF */
+-              fuse_reply_buf(req, NULL, 0);
++              DL(fuse_reply_buf)(req, NULL, 0);
+       } else {
+-              fuse_reply_buf(req, buf, osize);
++              DL(fuse_reply_buf)(req, buf, osize);
+       }
+       free(buf);
+ }
+ static void sqfs_ll_op_readlink(fuse_req_t req, fuse_ino_t ino) {
++      LOAD_SYMBOL(int,fuse_reply_err,(fuse_req_t req, int err));
++      LOAD_SYMBOL(int,fuse_reply_readlink,(fuse_req_t req, const char *link));
+       char *dst;
+       size_t size;
+       sqfs_ll_i lli;
+@@ -256,21 +278,24 @@
+               return;
+       
+       if (!S_ISLNK(lli.inode.base.mode)) {
+-              fuse_reply_err(req, EINVAL);
++              DL(fuse_reply_err)(req, EINVAL);
+       } else if (sqfs_readlink(&lli.ll->fs, &lli.inode, NULL, &size)) {
+-              fuse_reply_err(req, EIO);
++              DL(fuse_reply_err)(req, EIO);
+       } else if (!(dst = malloc(size + 1))) {
+-              fuse_reply_err(req, ENOMEM);
++              DL(fuse_reply_err)(req, ENOMEM);
+       } else if (sqfs_readlink(&lli.ll->fs, &lli.inode, dst, &size)) {
+-              fuse_reply_err(req, EIO);
++              DL(fuse_reply_err)(req, EIO);
+               free(dst);
+       } else {
+-              fuse_reply_readlink(req, dst);
++              DL(fuse_reply_readlink)(req, dst);
+               free(dst);
+       }
+ }
+ static void sqfs_ll_op_listxattr(fuse_req_t req, fuse_ino_t ino, size_t size) {
++      LOAD_SYMBOL(int,fuse_reply_err,(fuse_req_t req, int err));
++      LOAD_SYMBOL(int,fuse_reply_xattr,(fuse_req_t req, size_t count));
++      LOAD_SYMBOL(int,fuse_reply_buf,(fuse_req_t req, const char *buf, size_t size));
+       sqfs_ll_i lli;
+       char *buf;
+       int ferr;
+@@ -280,17 +305,17 @@
+       buf = NULL;
+       if (size && !(buf = malloc(size))) {
+-              fuse_reply_err(req, ENOMEM);
++              DL(fuse_reply_err)(req, ENOMEM);
+               return;
+       }
+       
+       ferr = sqfs_listxattr(&lli.ll->fs, &lli.inode, buf, &size);
+       if (ferr) {
+-              fuse_reply_err(req, ferr);
++              DL(fuse_reply_err)(req, ferr);
+       } else if (buf) {
+-              fuse_reply_buf(req, buf, size);
++              DL(fuse_reply_buf)(req, buf, size);
+       } else {
+-              fuse_reply_xattr(req, size);
++              DL(fuse_reply_xattr)(req, size);
+       }
+       free(buf);
+ }
+@@ -301,13 +326,16 @@
+               , uint32_t position
+ #endif
+               ) {
++      LOAD_SYMBOL(int,fuse_reply_err,(fuse_req_t req, int err));
++      LOAD_SYMBOL(int,fuse_reply_xattr,(fuse_req_t req, size_t count));
++      LOAD_SYMBOL(int,fuse_reply_buf,(fuse_req_t req, const char *buf, size_t size));
+       sqfs_ll_i lli;
+       char *buf = NULL;
+       size_t real = size;
+ #ifdef FUSE_XATTR_POSITION
+       if (position != 0) { /* We don't support resource forks */
+-              fuse_reply_err(req, EINVAL);
++              DL(fuse_reply_err)(req, EINVAL);
+               return;
+       }
+ #endif
+@@ -316,26 +344,27 @@
+               return;
+       
+       if (!(buf = malloc(size)))
+-              fuse_reply_err(req, ENOMEM);
++              DL(fuse_reply_err)(req, ENOMEM);
+       else if (sqfs_xattr_lookup(&lli.ll->fs, &lli.inode, name, buf, &real))
+-              fuse_reply_err(req, EIO);
++              DL(fuse_reply_err)(req, EIO);
+       else if (real == 0)
+-              fuse_reply_err(req, sqfs_enoattr());
++              DL(fuse_reply_err)(req, sqfs_enoattr());
+       else if (size == 0)
+-              fuse_reply_xattr(req, real);
++              DL(fuse_reply_xattr)(req, real);
+       else if (size < real)
+-              fuse_reply_err(req, ERANGE);
++              DL(fuse_reply_err)(req, ERANGE);
+       else
+-              fuse_reply_buf(req, buf, real);
++              DL(fuse_reply_buf)(req, buf, real);
+       free(buf);
+ }
+ static void sqfs_ll_op_forget(fuse_req_t req, fuse_ino_t ino,
+               unsigned long nlookup) {
++      LOAD_SYMBOL(void,fuse_reply_none,(fuse_req_t req));
+       sqfs_ll_i lli;
+       sqfs_ll_iget(req, &lli, SQFS_FUSE_INODE_NONE);
+       lli.ll->ino_forget(lli.ll, ino, nlookup);
+-      fuse_reply_none(req);
++      DL(fuse_reply_none)(req);
+ }
+@@ -348,23 +377,27 @@
+ static sqfs_err sqfs_ll_mount(sqfs_ll_chan *ch, const char *mountpoint,
+               struct fuse_args *args) {
++      LOAD_SYMBOL(struct fuse_chan *,fuse_mount,(const char *mountpoint, struct fuse_args *args));
+       #ifdef HAVE_NEW_FUSE_UNMOUNT
+-              ch->ch = fuse_mount(mountpoint, args);
++              ch->ch = DL(fuse_mount)(mountpoint, args);
+       #else
+-              ch->fd = fuse_mount(mountpoint, args);
++              LOAD_SYMBOL(struct fuse_chan *,fuse_kern_chan_new,(int fd));
++              ch->fd = DL(fuse_mount)(mountpoint, args);
+               if (ch->fd == -1)
+                       return SQFS_ERR;
+-              ch->ch = fuse_kern_chan_new(ch->fd);
++              ch->ch = DL(fuse_kern_chan_new)(ch->fd);
+       #endif
+       return ch->ch ? SQFS_OK : SQFS_ERR;
+ }
+ static void sqfs_ll_unmount(sqfs_ll_chan *ch, const char *mountpoint) {
+       #ifdef HAVE_NEW_FUSE_UNMOUNT
+-              fuse_unmount(mountpoint, ch->ch);
++              LOAD_SYMBOL(void,fuse_unmount,(const char *mountpoint, struct fuse_chan *ch));
++              DL(fuse_unmount)(mountpoint, ch->ch);
+       #else
++              LOAD_SYMBOL(void,fuse_unmount,(const char *mountpoint));
+               close(ch->fd);
+-              fuse_unmount(mountpoint);
++              DL(fuse_unmount)(mountpoint);
+       #endif
+ }
+@@ -391,6 +424,19 @@
+ }
+ int fusefs_main(int argc, char *argv[], void (*mounted) (void)) {
++      LOAD_SYMBOL(int,fuse_opt_parse,(struct fuse_args *args, void *data, const struct fuse_opt opts[], fuse_opt_proc_t proc));
++      LOAD_SYMBOL(int,fuse_parse_cmdline,(struct fuse_args *args, char **mountpoint, int *multithreaded, int *foreground));
++      LOAD_SYMBOL(struct fuse_session *,fuse_lowlevel_new,(struct fuse_args *args, const struct fuse_lowlevel_ops *op, size_t op_size, void *userdata));
++      LOAD_SYMBOL(int,fuse_set_signal_handlers,(struct fuse_session *se));
++      LOAD_SYMBOL(void,fuse_session_add_chan,(struct fuse_session *se, struct fuse_chan *ch));
++      LOAD_SYMBOL(int,fuse_session_loop,(struct fuse_session *se));
++      LOAD_SYMBOL(void,fuse_remove_signal_handlers,(struct fuse_session *se));
++#if HAVE_DECL_FUSE_SESSION_REMOVE_CHAN
++      LOAD_SYMBOL(void,fuse_session_remove_chan,(struct fuse_chan *ch));
++#endif
++      LOAD_SYMBOL(void,fuse_session_destroy,(struct fuse_session *se));
++      LOAD_SYMBOL(void,fuse_opt_free_args,(struct fuse_args *args));
++
+       struct fuse_args args;
+       sqfs_opts opts;
+       
+@@ -429,10 +475,10 @@
+       opts.image = NULL;
+       opts.mountpoint = 0;
+       opts.offset = 0;
+-      if (fuse_opt_parse(&args, &opts, fuse_opts, sqfs_opt_proc) == -1)
++      if (DL(fuse_opt_parse)(&args, &opts, fuse_opts, sqfs_opt_proc) == -1)
+               sqfs_usage(argv[0], true);
+-      if (fuse_parse_cmdline(&args, &mountpoint, &mt, &fg) == -1)
++      if (DL(fuse_parse_cmdline)(&args, &mountpoint, &mt, &fg) == -1)
+               sqfs_usage(argv[0], true);
+       if (mountpoint == NULL)
+               sqfs_usage(argv[0], true);
+@@ -445,33 +491,34 @@
+               sqfs_ll_chan ch;
+               err = -1;
+               if (sqfs_ll_mount(&ch, mountpoint, &args) == SQFS_OK) {
+-                      struct fuse_session *se = fuse_lowlevel_new(&args,
++                      struct fuse_session *se = DL(fuse_lowlevel_new)(&args,
+                               &sqfs_ll_ops, sizeof(sqfs_ll_ops), ll); 
+                       if (se != NULL) {
+                               if (sqfs_ll_daemonize(fg) != -1) {
+-                                      if (fuse_set_signal_handlers(se) != -1) {
+-                                              fuse_session_add_chan(se, ch.ch);
++                                      if (DL(fuse_set_signal_handlers)(se) != -1) {
++                                              DL(fuse_session_add_chan)(se, ch.ch);
+                               if (mounted)
+                                 mounted ();
+                                               /* FIXME: multithreading */
+-                                              err = fuse_session_loop(se);
+-                                              fuse_remove_signal_handlers(se);
++                                              err = DL(fuse_session_loop)(se);
++                                              DL(fuse_remove_signal_handlers)(se);
+                                               #if HAVE_DECL_FUSE_SESSION_REMOVE_CHAN
+-                                                      fuse_session_remove_chan(ch.ch);
++                                                      DL(fuse_session_remove_chan)(ch.ch);
+                                               #endif
+                                       }
+                               }
+-                              fuse_session_destroy(se);
++                              DL(fuse_session_destroy)(se);
+                       }
+                       sqfs_ll_destroy(ll);
+                       sqfs_ll_unmount(&ch, mountpoint);
+               }
+       }
+-      fuse_opt_free_args(&args);
++      DL(fuse_opt_free_args)(&args);
+       if (mounted)
+         rmdir (mountpoint);
+       free(ll);
+       free(mountpoint);
++      CLOSE_LIBRARY;
+       
+       return -err;
+ }
diff --git a/src/xdg-basedir/CMakeLists.txt b/src/xdg-basedir/CMakeLists.txt
new file mode 100644 (file)
index 0000000..064260d
--- /dev/null
@@ -0,0 +1,7 @@
+cmake_minimum_required(VERSION 3.5)
+
+set(CMAKE_POSITION_INDEPENDENT_CODE ON)
+
+# force static linking
+add_library(xdg-basedir STATIC xdg-basedir.h xdg-basedir.c)
+target_include_directories(xdg-basedir PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
diff --git a/src/xdg-basedir/xdg-basedir.c b/src/xdg-basedir/xdg-basedir.c
new file mode 100644 (file)
index 0000000..c154c76
--- /dev/null
@@ -0,0 +1,67 @@
+#include "xdg-basedir.h"
+#include <string.h>
+#include <stdlib.h>
+
+char* user_home() {
+    return strdup(getenv("HOME"));
+}
+
+char* xdg_config_home()  {
+    char* config_home = getenv("XDG_CONFIG_HOME");
+
+    if (config_home == NULL) {
+        char* home = user_home();
+        static const char const* suffix = "/.config";
+
+        config_home = calloc(strlen(home) + strlen(suffix) + 1, sizeof(char));
+
+        strcpy(config_home, home);
+        strcat(config_home, suffix);
+
+        free(home);
+
+        return config_home;
+    } else {
+        return strdup(config_home);
+    }
+}
+
+char* xdg_data_home() {
+    char* data_home = getenv("XDG_DATA_HOME");
+
+    if (data_home == NULL) {
+        char* home = user_home();
+        static const char const* suffix = "/.local/share";
+
+        data_home = calloc(strlen(home) + strlen(suffix) + 1, sizeof(char));
+
+        strcpy(data_home, home);
+        strcat(data_home, suffix);
+
+        free(home);
+
+        return data_home;
+    } else {
+        return strdup(data_home);
+    }
+}
+
+char* xdg_cache_home() {
+    char* cache_home = getenv("XDG_CACHE_HOME");
+
+    if (cache_home == NULL) {
+        char* home = user_home();
+        static const char const* suffix = "/.cache";
+
+        cache_home = calloc(strlen(home) + strlen(suffix) + 1, sizeof(char));
+
+        strcpy(cache_home, home);
+        strcat(cache_home, suffix);
+
+        free(home);
+
+        return cache_home;
+    } else {
+        return strdup(cache_home);
+    }
+}
diff --git a/src/xdg-basedir/xdg-basedir.h b/src/xdg-basedir/xdg-basedir.h
new file mode 100644 (file)
index 0000000..17c5659
--- /dev/null
@@ -0,0 +1,40 @@
+#ifndef XDG_BASEDIR_H
+#define XDG_BASEDIR_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*
+ * Get user's home directory. Convenience wrapper for getenv("HOME").
+ * Returns a freshly allocated char array that must be free'd after usage.
+ */
+char* user_home();
+
+/*
+ * Get XDG config home directory using $XDG_CONFIG_HOME environment variable.
+ * Falls back to default value ~/.config if environment variable is not set.
+ * Returns a freshly allocated char array that must be free'd after usage.
+ */
+char* xdg_config_home();
+
+/*
+ * Get XDG data home directory using $XDG_DATA_HOME environment variable.
+ * Falls back to default value ~/.local/share if environment variable is not set.
+ * Returns a freshly allocated char array that must be free'd after usage.
+ */
+char* xdg_data_home();
+
+/*
+ * Get XDG cache home directory using $XDG_CACHE_HOME environment variable.
+ * Falls back to default value ~/.cache if environment variable is not set.
+ * Returns a freshly allocated char array that must be free'd after usage.
+ */
+char* xdg_cache_home();
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* XDG_BASEDIR_H */
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
new file mode 100644 (file)
index 0000000..cb48305
--- /dev/null
@@ -0,0 +1,19 @@
+cmake_minimum_required(VERSION 3.5)
+
+# global definitions
+add_definitions(
+    -DTEST_DATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data/"
+    -DGIT_COMMIT="AppImageKit unit tests"
+)
+
+
+add_subdirectory(libappimage)
+
+if(ENABLE_COVERAGE)
+    set(COVERAGE_LCOV_EXCLUDES '${PROJECT_SOURCE_DIR}/lib/*' '${PROJECT_SOURCE_DIR}/tests/*' '${PROJECT_SOURCE_DIR}/*build*' '/usr/*')
+    setup_target_for_coverage_lcov(
+        NAME coverage
+        EXECUTABLE ctest -V -j ${PROCESSOR_COUNT}
+        DEPENDENCIES test_libappimage++ TestDesktopIntegration test-xdg-basedir test_shared test_libappimage test_libappimage
+    )
+endif()
diff --git a/tests/client_app/CMakeLists.txt b/tests/client_app/CMakeLists.txt
new file mode 100644 (file)
index 0000000..b5ed361
--- /dev/null
@@ -0,0 +1,7 @@
+project(client_app)
+cmake_minimum_required(VERSION 3.0)
+
+find_package(libappimage REQUIRED)
+
+add_executable(client_app main.c)
+target_link_libraries(client_app libappimage)
diff --git a/tests/client_app/main.c b/tests/client_app/main.c
new file mode 100644 (file)
index 0000000..679ef23
--- /dev/null
@@ -0,0 +1,6 @@
+#include <appimage/appimage.h>
+
+int main(int argc, char** argv) {
+    appimage_get_md5("Hello World");
+    return 0;
+}
diff --git a/tests/data/AppImageExtract_6-x86_64.AppImage b/tests/data/AppImageExtract_6-x86_64.AppImage
new file mode 100755 (executable)
index 0000000..20c2698
Binary files /dev/null and b/tests/data/AppImageExtract_6-x86_64.AppImage differ
diff --git a/tests/data/AppImageExtract_6_no_magic_bytes-x86_64.AppImage b/tests/data/AppImageExtract_6_no_magic_bytes-x86_64.AppImage
new file mode 100755 (executable)
index 0000000..f512825
Binary files /dev/null and b/tests/data/AppImageExtract_6_no_magic_bytes-x86_64.AppImage differ
diff --git a/tests/data/Cura.desktop b/tests/data/Cura.desktop
new file mode 100644 (file)
index 0000000..1a9b615
--- /dev/null
@@ -0,0 +1,15 @@
+[Desktop Entry]
+Name=Ultimaker Cura
+Name[de]=Ultimaker Cura
+GenericName=3D Printing Software
+GenericName[de]=3D-Druck-Software
+Comment=Cura converts 3D models into paths for a 3D printer. It prepares your print for maximum accuracy, minimum printing time and good reliability with many extra features that make your print come out great.
+Comment[de]=Cura wandelt 3D-Modelle in Pfade fรผr einen 3D-Drucker um. Es bereitet Ihren Druck fรผr maximale Genauigkeit, minimale Druckzeit und guter Zuverlรคssigkeit mit vielen zusรคtzlichen Funktionen vor, damit Ihr Druck groรŸartig wird.
+Exec=cura %F
+TryExec=cura
+Icon=cura-icon
+Terminal=false
+Type=Application
+MimeType=application/sla;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;image/bmp;image/gif;image/jpeg;image/png;model/x3d+xml;
+Categories=Graphics;
+Keywords=3D;Printing;
diff --git a/tests/data/Echo-no-integrate-x86_64.AppImage b/tests/data/Echo-no-integrate-x86_64.AppImage
new file mode 100755 (executable)
index 0000000..c4c9a79
Binary files /dev/null and b/tests/data/Echo-no-integrate-x86_64.AppImage differ
diff --git a/tests/data/Echo-test1234-x86_64.AppImage b/tests/data/Echo-test1234-x86_64.AppImage
new file mode 100755 (executable)
index 0000000..4e893a3
Binary files /dev/null and b/tests/data/Echo-test1234-x86_64.AppImage differ
diff --git a/tests/data/Echo-x86_64.AppImage b/tests/data/Echo-x86_64.AppImage
new file mode 100755 (executable)
index 0000000..bd07c23
Binary files /dev/null and b/tests/data/Echo-x86_64.AppImage differ
diff --git a/tests/data/appimaged-i686.AppImage b/tests/data/appimaged-i686.AppImage
new file mode 100755 (executable)
index 0000000..40cde74
Binary files /dev/null and b/tests/data/appimaged-i686.AppImage differ
diff --git a/tests/data/appimagetool-x86_64.AppImage b/tests/data/appimagetool-x86_64.AppImage
new file mode 100755 (executable)
index 0000000..d07dcc7
Binary files /dev/null and b/tests/data/appimagetool-x86_64.AppImage differ
diff --git a/tests/data/broken-desktop-file-x86_64.AppImage b/tests/data/broken-desktop-file-x86_64.AppImage
new file mode 100755 (executable)
index 0000000..ec6cd42
Binary files /dev/null and b/tests/data/broken-desktop-file-x86_64.AppImage differ
diff --git a/tests/data/elffile b/tests/data/elffile
new file mode 100755 (executable)
index 0000000..ee3f388
Binary files /dev/null and b/tests/data/elffile differ
diff --git a/tests/data/minimal.iso b/tests/data/minimal.iso
new file mode 100644 (file)
index 0000000..a5150b5
Binary files /dev/null and b/tests/data/minimal.iso differ
diff --git a/tests/data/squashfs-root/.DirIcon b/tests/data/squashfs-root/.DirIcon
new file mode 120000 (symlink)
index 0000000..08ba74f
--- /dev/null
@@ -0,0 +1 @@
+utilities-terminal.svg
\ No newline at end of file
diff --git a/tests/data/squashfs-root/AppRun b/tests/data/squashfs-root/AppRun
new file mode 100755 (executable)
index 0000000..67e971b
Binary files /dev/null and b/tests/data/squashfs-root/AppRun differ
diff --git a/tests/data/squashfs-root/echo.desktop b/tests/data/squashfs-root/echo.desktop
new file mode 100644 (file)
index 0000000..0707d31
--- /dev/null
@@ -0,0 +1,9 @@
+[Desktop Entry]
+Version=1.0
+Type=Application
+Name=Echo
+Name[de]=Echo DE
+Comment=Just echo.
+Exec=echo %F
+Icon=utilities-terminal
+X-AppImage-Version=1234
diff --git a/tests/data/squashfs-root/usr/bin/echo b/tests/data/squashfs-root/usr/bin/echo
new file mode 100755 (executable)
index 0000000..44f4be9
Binary files /dev/null and b/tests/data/squashfs-root/usr/bin/echo differ
diff --git a/tests/data/squashfs-root/utilities-terminal.png b/tests/data/squashfs-root/utilities-terminal.png
new file mode 100644 (file)
index 0000000..8cb0e0f
Binary files /dev/null and b/tests/data/squashfs-root/utilities-terminal.png differ
diff --git a/tests/data/squashfs-root/utilities-terminal.svg b/tests/data/squashfs-root/utilities-terminal.svg
new file mode 100644 (file)
index 0000000..4a0250d
--- /dev/null
@@ -0,0 +1,320 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created using Karbon, part of Calligra: http://www.calligra.org/karbon -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="48"
+   height="48"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="utilities-terminal.svg"
+   inkscape:export-filename="/home/uri/Documentos/icon-shadows/48/utilities-terminal.png"
+   inkscape:export-xdpi="720"
+   inkscape:export-ydpi="720">
+  <metadata
+     id="metadata12">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10000"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="2560"
+     inkscape:window-height="1018"
+     id="namedview10"
+     showgrid="true"
+     inkscape:showpageshadow="false"
+     borderlayer="true"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     inkscape:zoom="17.666667"
+     inkscape:cx="24"
+     inkscape:cy="24"
+     inkscape:window-x="0"
+     inkscape:window-y="32"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="layer1"
+     showguides="true">
+    <inkscape:grid
+       type="xygrid"
+       id="grid4141"
+       originx="20px"
+       originy="0px" />
+    <sodipodi:guide
+       position="0,48"
+       orientation="48,0"
+       id="guide4147" />
+    <sodipodi:guide
+       position="48,0"
+       orientation="-48,0"
+       id="guide4151" />
+    <sodipodi:guide
+       position="48,48"
+       orientation="0,-48"
+       id="guide4153" />
+    <sodipodi:guide
+       position="4,37"
+       orientation="40,0"
+       id="guide4219" />
+    <sodipodi:guide
+       position="6.9999962,3.999959"
+       orientation="0,34"
+       id="guide4221" />
+    <sodipodi:guide
+       position="44,32"
+       orientation="-29,0"
+       id="guide4223" />
+    <sodipodi:guide
+       position="29.999996,43.999958"
+       orientation="0,-23"
+       id="guide4227" />
+  </sodipodi:namedview>
+  <defs
+     id="defs4">
+    <linearGradient
+       inkscape:collect="always"
+       id="Shadow">
+      <stop
+         style="stop-color:#000000;stop-opacity:1;"
+         offset="0"
+         id="stop4186" />
+      <stop
+         style="stop-color:#000000;stop-opacity:0;"
+         offset="1"
+         id="stop4188" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#Shadow"
+       id="linearGradient4334"
+       x1="12"
+       y1="21"
+       x2="12"
+       y2="23"
+       gradientUnits="userSpaceOnUse" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#Shadow"
+       id="linearGradient4338"
+       gradientUnits="userSpaceOnUse"
+       x1="12"
+       y1="21"
+       x2="12"
+       y2="23"
+       gradientTransform="translate(0,3.9999904)" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#Shadow"
+       id="linearGradient4342"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="translate(0,5.9999816)"
+       x1="12"
+       y1="21"
+       x2="12"
+       y2="23" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#Shadow"
+       id="linearGradient4346"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="translate(4,5.9999816)"
+       x1="12"
+       y1="21"
+       x2="12"
+       y2="23" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#Shadow"
+       id="linearGradient4350"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="translate(10,5.9999816)"
+       x1="12"
+       y1="21"
+       x2="12"
+       y2="23" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#Shadow"
+       id="linearGradient4354"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="translate(10,2.9999816)"
+       x1="12"
+       y1="21"
+       x2="12"
+       y2="23" />
+  </defs>
+  <g
+     inkscape:groupmode="layer"
+     id="layer1"
+     inkscape:label="Layer 1">
+    <image
+       y="0"
+       x="0"
+       id="image3262"
+       xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAYAAAAGACAYAAACkx7W/AAAABHNCSVQICAgIfAhkiAAAAAlwSFlz
+AABuugAAbroB1t6xFwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAvDSURB
+VHic7dzRcqO4GoVRksz7v3En52KaOhpFEiIGjLPXqnLZ7STYvvk/ELiXBQAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAACA87w9+w3s8ErvFcj29ew3MOPOQ3X2vd35MwC/2+ygv2UQ7jY8
+e+9n633e7XMAv9/WUO/9/DYxuMPgbL2H+rmZ3wF4ltZQr5+b+Z1LPXuIjgb92+C53r8BrjYa9F+D
+53r/vsyzBmhvkM/et7Yx8zoAe80M6NHQ37rf8zqHunpAzgz+mce97W29HsBee9b6y+E+87i1/ctC
+8M9VL7SMl3felvbAr2+tv60fA1xha6+/vtXP97b31vn54a4anL3h/9Nbb5sAVxnt+f/k1tvmaa44
+Apgd/u/Vff145kgA4Eoze/6fjcefnW2Ve/+nHwmcHYB6+JdDuzXo35fvIZiJQO81AY40WrrpDf/3
+5f+D/+3v47flexha2zw1AmcGoDX8W3v99dBvPWcZCLiT2eWfco9/HfzrUH8rnn8v/qbe7mkROCsA
+W8O/Hvgfy/fhLwDAXf00AOWtnGPrc73XOSUCVywBjfb6WwEoH28FYFkMfuB56hBsBeDP8n2OlYN/
+FIHDnRGA3t55Pfw/OvdbRwG91wB4ht6J4HL9vxz6ZQiW5fscWyNQ7/0ffhRwdADqD9Lb8y+H/sfS
+j0Bv+LdeB+BKrS9wlRFYh/96Xx8F/Blsu1wiKl/n0AiceQ5ga73/o3Fr7f3PXP0jAMDVet/gLZeA
+yr338jYa/us21nD0Xu9hRwag3kPfisBWAMq/K7dbvx7AM5Qz6Gv5vsdeLgX1zmHW1r8pl47qnx92
+FHD2ZaC95Z+tABj+wCsph3L9eF333xOA9XFrGegwRwVga++/HPwzyz8u+QReXRmC92V7iH8t/87C
+8t/L8v1I4LCjgLPPAdTf9h2dAG4FYGncj14T4Aqt4dubQeuwHg3sdfiPlo1ueQ5gtPe/deln77JP
+J3yBO+vNoa+Nn783fr8c/u/VfX0Cufy7h6Nw9vcAekP+J8Pf4Afurl6zL68CWpURKK8YWneO6wCc
+9m3gM84BrPe9o4CZSz0NfuCVlSFozbByNpYBWO9bXyCrLyt9OAb14cherUG9d/j3TvrW2wd4NfU8
+G10eP7OD3Nrmjz0agFrvA/aeq0/41tsCeHVbRwCz87I3K3/syAD0Sje71z868QvwylorJOW/e0cD
+p66MHBGA1ocZ3XofCiDJzN7+aCf54fn5SAB6Lz7zAXrRGG0X4JVtHQXMzszednc7eglodMLjlIIB
+vLDZELR+/2FHnwRelvm9/t7fAvxWW7PvJ0cAP3ZUAEZvzJEAwH/N7Plv/f3Dzr4M9JIPAfCi9uw8
+b/3+bmddBtr6mZO8AH1bqyKHz9AzzgGUWicwAGg77YRvy5H/FUT9/OwRAUCyR5bLH5qjR30RbOZ3
+DHyAbbMrJw/P1LOXgPYQCCDBbWbdnQIAwIXOugy0fg6AfVqz9LaXgQLwQgQAIJQAAIQSAIBQAgAQ
+SgAAQgkAQCgBAAglAAChBAAglAAAhBIAgFACABBKAABCCQBAKAEACCUAAKEEACCUAACEEgCAUAIA
+EEoAAEIJAEAoAQAIJQAAoQQAIJQAAIQSAIBQAgAQSgAAQgkAQCgBAAglAAChBAAglAAAhBIAgFAC
+ABBKAABCCQBAKAEACCUAAKEEACCUAACEEgCAUAIAEEoAAEIJAEAoAQAIJQAAoQQAIJQAAIQSAIBQ
+AgAQSgAAQgkAQCgBAAglAAChBAAglAAAhBIAgFACABBKAABCCQBAKAEACCUAAKEEACCUAACEEgCA
+UAIAEEoAAEIJAEAoAQAIJQAAoQQAIJQAAIQSAIBQAgAQSgAAQgkAQCgBAAglAAChBAAglAAAhBIA
+gFACABBKAABCCQBAKAEACCUAAKEEACCUAACEEgCAUAIAEEoAAEIJAEAoAQAIJQAAoQQAIJQAAIQS
+AIBQAgAQSgAAQgkAQCgBAAglAAChBAAglAAAhBIAgFACABBKAABCCQBAKAEACCUAAKEEACCUAACE
+EgCAUAIAEEoAAEIJAEAoAQAIJQAAoQQAIJQAAIQSAIBQAgAQSgAAQgkAQCgBAAglAAChBAAglAAA
+hBIAgFACABBKAABCCQBAKAEACCUAAKEEACCUAACEEgCAUAIAEEoAAEIJAEAoAQAIJQAAoQQAIJQA
+AIQSAIBQAgAQSgAAQgkAQCgBAAglAAChBAAglAAAhBIAgFACABBKAABCCQBAKAEACCUAAKEEACCU
+AACEEgCAUAIAEEoAAEIJAEAoAQAIJQAAoQQAIJQAAIQSAIBQAgAQSgAAQgkAQCgBAAglAAChBAAg
+lAAAhBIAgFACABBKAABCCQBAKAEACCUAAKEEACCUAACEEgCAUAIAEEoAAEIJAEAoAQAIJQAAoQQA
+IJQAAIQSAIBQAgAQSgAAQgkAQCgBAAglAAChBAAglAAAhBIAgFACABBKAABCCQBAKAEACCUAAKEE
+ACCUAACEEgCAUAIAEEoAAEIJAEAoAQAIJQAAoQQAIJQAAIQSAIBQAgAQSgAAQgkAQCgBAAglAACh
+BAAglAAAhBIAgFACABDq6AB8/b3VzwGwT2uWHjpPHQEAhLpTABwpAAluM+uOCMDMhzn80AXgl6rn
+ZW92PjxTHw3A6I2N3pwgAPxrZh6eEoGzl4BmSwbA93l56sw8MgCze/wiAPBdOSNbc/LwGXrWZaD1
+bfT7AKlmdpxP24E+KgAz61e9MDgfAKRpzcDejvPoaOAhZ5wD6H2YmYoJAfCbbc2+S3eUjz4H0DqB
+Yc8foG3mSOC0E8OPBGDrEtCZW/37o+0CvLLRzNszM3vb3e2oL4Lt+SCfjecA0szMxlNXUM66DLT+
+UOWt/qD179fbAnh1W3v/5Wys5+XMSeEfOfsy0FbRPjuPW9sCeHW9+Taah5esmHwcsI23v7elup+9
+1dtYBo8BXklvZaS11/+nuP9T/Ls8Gmht88eOCkDvfu+tt+36McCdtYZ075xoLwD1clC9rYcdHYDR
+41EY6m3V2229HsDd1IO5NfzrwV4GoIzA6DzAIY4IwLK0h/Ro6Ld+p7etrednfw5wlK1hXO+pzwz/
+1v3WlUAPOToAM+cDyp/Vf7/39QDuYmbZp7XH39rrP33vf1mOC8CyjIf6zFp+7+9dDQS8gtYJ3/Vx
+64Rvfeut/5+y978s5wTgqCt6Rme6RQG4kz3DvxeA0eA/xZEBWJa5K3n2RmDmpArA1Xpf6qqXe1pr
+/6Ph37v+//Avyv5z1IYq5Rv8XP79wtn6oUZ/87H898O+/f3b0XcGlsa/Ac7W2jltnfTdutyzd+XP
+6UcBRwdgHdrlv9f72eH/Xt3WgPROMAM80+iKn96VP72rfraWfw4NwRlHAGsEyhisb7qOwPoBy+Ff
+HgWsw/9zmfvuAMCVRt/07Z38bT1uDf/6NQ4/CjhrCWjVesPlN9p662fr3n+9BDS6nFQIgKvMXPI5
+exQws+5/ijPPAYwu4/xcvg/sdW+/vAkAcEePBqB+rvW3oyshD3H20OxdAVQP9nLY14O/FYB6e73X
+BDhSaxBvXQnUCkG9x1//Z2+nD/9luWZY9v5/n/rWGvj2/oG72nsU0ArC1mWepy4BXTU4ZyMwe+tt
+E+AKvatzeks5M7d6u6cO/2W5dnD2IrDet5Z3RoNfAIBnGl2pszXkW0P/0uG/LNcPzt4XuFrr+qPH
+ve1tvR7AHjODeLQUtPW49RqXDP9led6AnAnB6L61jZnXAdhjdhhvHQ2M7ve+1mGePSBH/6XDzNB/
+9vsHGA3ymeWdywf/6g4DdOZSTpd7AnfWuzx07+9c6m5DtPd+rPUDd7M1vHs/f+rQL915cM6+tzt/
+BuB3+8k5gtt4peH5Su8VyHbLgQ8AAAAAAAAAAAAAAAAAAAAAAAAAADzL/wA+ZrMqzkVe+gAAAABJ
+RU5ErkJggg==
+"
+       preserveAspectRatio="none"
+       height="48"
+       width="48" />
+    <rect
+       style="opacity:1;fill:#37474f;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="rect4272"
+       width="42"
+       height="36.000042"
+       x="3"
+       y="8"
+       ry="3" />
+    <path
+       style="opacity:1;fill:#455a64;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="M 44 14 L 4 14 L 4 41 C 4 42.108 4.892 43 6 43 L 42 43 C 43.108 43 44 42.108 44 41 L 44 14 z "
+       id="path4285" />
+    <path
+       style="opacity:1;fill:#e0e0e0;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="M 6 9 C 4.892 9 4 9.892 4 11 L 4 14 L 44 14 L 44 11 C 44 9.892 43.108 9 42 9 L 6 9 z "
+       id="rect4274" />
+    <rect
+       style="opacity:1;fill:#37474f;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="rect4276"
+       width="40"
+       height="1"
+       x="4"
+       y="14.000004"
+       ry="0" />
+    <circle
+       style="opacity:1;fill:#ef5350;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="path4278"
+       cx="41.499985"
+       cy="11.500004"
+       r="1.5" />
+    <circle
+       r="0.5"
+       cy="11.500004"
+       cx="41.499985"
+       id="circle4280"
+       style="opacity:1;fill:#ef9a9a;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+    <rect
+       style="opacity:1;fill:url(#linearGradient4334);fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="rect4326"
+       width="10"
+       height="1"
+       x="7"
+       y="22" />
+    <rect
+       y="26.000004"
+       x="7"
+       height="1"
+       width="10"
+       id="rect4336"
+       style="opacity:1;fill:url(#linearGradient4338);fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+    <path
+       style="opacity:1;fill:#b0bec5;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="M 9 18 L 9 20 L 7 20 L 7 22 L 9 22 L 9 24 L 7 24 L 7 26 L 9 26 L 9 28 L 11 28 L 11 26 L 13 26 L 13 28 L 15 28 L 15 26 L 17 26 L 17 24 L 15 24 L 15 22 L 17 22 L 17 20 L 15 20 L 15 18 L 13 18 L 13 20 L 11 20 L 11 18 L 9 18 z M 19 18 L 19 25 L 21 25 L 21 18 L 19 18 z M 11 22 L 13 22 L 13 24 L 11 24 L 11 22 z M 19 26 L 19 28 L 21 28 L 21 26 L 19 26 z "
+       id="rect4309" />
+    <rect
+       style="opacity:1;fill:url(#linearGradient4342);fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="rect4340"
+       width="2"
+       height="1"
+       x="9"
+       y="28" />
+    <rect
+       y="28"
+       x="13"
+       height="1"
+       width="2"
+       id="rect4344"
+       style="opacity:1;fill:url(#linearGradient4346);fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+    <rect
+       style="opacity:1;fill:url(#linearGradient4350);fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="rect4348"
+       width="2"
+       height="1"
+       x="19"
+       y="28" />
+    <rect
+       y="25.000004"
+       x="19"
+       height="1"
+       width="2"
+       id="rect4352"
+       style="opacity:1;fill:url(#linearGradient4354);fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+  </g>
+</svg>
diff --git a/tests/libappimage/CMakeLists.txt b/tests/libappimage/CMakeLists.txt
new file mode 100644 (file)
index 0000000..a5b4b52
--- /dev/null
@@ -0,0 +1,29 @@
+add_subdirectory(legacy)
+
+if (NOT LIBAPPIMAGE_SHARED_ONLY)
+    if(LIBAPPIMAGE_DESKTOP_INTEGRATION_ENABLED)
+        add_subdirectory(desktop_integration)
+    endif()
+
+    add_executable(
+        test_libappimage++
+
+        TestLibappimage++.cpp
+
+        core/impl/TestTraversalType1.cpp
+        core/impl/TestTraversalType2.cpp
+
+        utils/TestMagicBytesChecker.cpp
+        utils/TestUtilsElf.cpp
+        utils/TestIconHandle.cpp
+        utils/TestLogger.cpp
+        utils/TestPayloadEntriesCache.cpp
+        utils/TestResourcesExtractor.cpp
+        utils/StringSanitizerTest.cpp
+    )
+
+    target_include_directories(test_libappimage++ PRIVATE "${PROJECT_SOURCE_DIR}/src/libappimage")
+    target_link_libraries(test_libappimage++ libappimage Boost::filesystem libarchive libsquashfuse XdgUtils::DesktopEntry XdgUtils::BaseDir gtest gtest_main)
+
+    add_test(test_libappimage++ test_libappimage++)
+endif()
diff --git a/tests/libappimage/TestLibappimage++.cpp b/tests/libappimage/TestLibappimage++.cpp
new file mode 100644 (file)
index 0000000..9cb96ce
--- /dev/null
@@ -0,0 +1,254 @@
+// system
+#include <vector>
+#include <fstream>
+#include <random>
+#include <string>
+
+// library
+#include <gtest/gtest.h>
+#include <boost/filesystem.hpp>
+
+// local
+#include <appimage/core/exceptions.h>
+#include <appimage/core/AppImage.h>
+
+using namespace appimage;
+namespace bf = boost::filesystem;
+
+class AppImageTests : public ::testing::Test {
+protected:
+
+    void SetUp() override {}
+
+    void TearDown() override {}
+
+    std::string random_string(std::string::size_type length) {
+        static auto& chrs = "0123456789"
+                            "abcdefghijklmnopqrstuvwxyz"
+                            "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+
+        thread_local static std::mt19937 rg{std::random_device{}()};
+        thread_local static std::uniform_int_distribution<std::string::size_type> pick(0, sizeof(chrs) - 2);
+
+        std::string s;
+
+        s.reserve(length);
+
+        while (length--)
+            s += chrs[pick(rg)];
+
+        return s;
+    }
+
+    std::string getTmpFilePath() {
+        std::string tmpFilePath = "/tmp/libappimage-test-" + random_string(16);
+        return tmpFilePath;
+    }
+
+    std::ifstream::pos_type fileSize(const std::string& filename) {
+        std::ifstream in(filename, std::ifstream::ate | std::ifstream::binary);
+        return in.tellg();
+    }
+};
+
+TEST_F(AppImageTests, instantiate) {
+    ASSERT_NO_THROW(core::AppImage(TEST_DATA_DIR
+                        "/AppImageExtract_6-x86_64.AppImage"));
+    ASSERT_NO_THROW(core::AppImage(TEST_DATA_DIR
+                        "/Echo-x86_64.AppImage"));
+    ASSERT_THROW(core::AppImage(TEST_DATA_DIR
+                     "/elffile"), core::AppImageError);
+    ASSERT_THROW(core::AppImage(TEST_DATA_DIR
+                     "/minimal.iso"), core::AppImageError);
+    ASSERT_THROW(core::AppImage(TEST_DATA_DIR
+                     "/Cura.desktop"), core::AppImageError);
+    ASSERT_THROW(core::AppImage(TEST_DATA_DIR
+                     "/none"), core::AppImageError);
+}
+
+TEST_F(AppImageTests, getFormat) {
+    ASSERT_EQ(core::AppImage(TEST_DATA_DIR
+                  "/AppImageExtract_6-x86_64.AppImage").getFormat(), core::AppImageFormat::TYPE_1);
+    ASSERT_EQ(core::AppImage(TEST_DATA_DIR
+                  "/AppImageExtract_6_no_magic_bytes-x86_64.AppImage").getFormat(), core::AppImageFormat::TYPE_1);
+    ASSERT_EQ(core::AppImage(TEST_DATA_DIR
+                  "/Echo-x86_64.AppImage").getFormat(), core::AppImageFormat::TYPE_2);
+    ASSERT_EQ(core::AppImage(TEST_DATA_DIR
+                  "/appimaged-i686.AppImage").getFormat(), core::AppImageFormat::TYPE_2);
+    ASSERT_THROW(core::AppImage(TEST_DATA_DIR
+                     "/elffile").getFormat(), core::AppImageError);
+    ASSERT_THROW(core::AppImage(TEST_DATA_DIR
+                     "/minimal.iso").getFormat(), core::AppImageError);
+    ASSERT_THROW(core::AppImage(TEST_DATA_DIR
+                     "/Cura.desktop").getFormat(), core::AppImageError);
+    ASSERT_THROW(core::AppImage(TEST_DATA_DIR
+                     "/non_existend_file").getFormat(), core::AppImageError);
+}
+
+TEST_F(AppImageTests, getFormatStatic) {
+    ASSERT_EQ(core::AppImage::getFormat(TEST_DATA_DIR
+                  "/AppImageExtract_6-x86_64.AppImage"), core::AppImageFormat::TYPE_1);
+    ASSERT_EQ(core::AppImage::getFormat(TEST_DATA_DIR
+                  "/AppImageExtract_6_no_magic_bytes-x86_64.AppImage"), core::AppImageFormat::TYPE_1);
+    ASSERT_EQ(core::AppImage::getFormat(TEST_DATA_DIR
+                  "/Echo-x86_64.AppImage"), core::AppImageFormat::TYPE_2);
+    ASSERT_EQ(core::AppImage::getFormat(TEST_DATA_DIR
+                  "/appimaged-i686.AppImage"), core::AppImageFormat::TYPE_2);
+    ASSERT_EQ(core::AppImage::getFormat(TEST_DATA_DIR
+                  "/elffile"), core::AppImageFormat::INVALID);
+    ASSERT_EQ(core::AppImage::getFormat(TEST_DATA_DIR
+                  "/minimal.iso"), core::AppImageFormat::INVALID);
+    ASSERT_EQ(core::AppImage::getFormat(TEST_DATA_DIR
+                  "/Cura.desktop"), core::AppImageFormat::INVALID);
+    ASSERT_EQ(core::AppImage::getFormat(TEST_DATA_DIR
+                  "/non_existend_file"), core::AppImageFormat::INVALID);
+}
+
+TEST_F(AppImageTests, getPayloadOffset) {
+    ASSERT_EQ(core::AppImage(TEST_DATA_DIR
+                  "/AppImageExtract_6-x86_64.AppImage").getPayloadOffset(), 28040);
+    ASSERT_EQ(core::AppImage(TEST_DATA_DIR
+                  "/AppImageExtract_6_no_magic_bytes-x86_64.AppImage").getPayloadOffset(), 28040);
+    ASSERT_EQ(core::AppImage(TEST_DATA_DIR
+                  "/Echo-x86_64.AppImage").getPayloadOffset(), 187784);
+    ASSERT_THROW(core::AppImage(TEST_DATA_DIR
+                     "/elffile").getPayloadOffset(), core::AppImageError);
+}
+
+TEST_F(AppImageTests, listType1Entries) {
+    core::AppImage appImage(TEST_DATA_DIR "/AppImageExtract_6-x86_64.AppImage");
+    std::set<std::string> expected = {
+        "AppImageExtract.desktop",
+        ".DirIcon",
+        "AppImageExtract.png",
+        "usr/bin/appimageextract",
+        "AppRun",
+        "usr/bin/xorriso",
+        "usr/lib/libburn.so.4",
+        "usr/lib/libisoburn.so.1",
+        "usr/lib/libisofs.so.6",
+    };
+
+    for (const auto& file: appImage.files())
+        expected.erase(file);
+
+    ASSERT_TRUE(expected.empty());
+}
+
+TEST_F(AppImageTests, listType2Entries) {
+    core::AppImage appImage(TEST_DATA_DIR "/Echo-x86_64.AppImage");
+    std::set<std::string> expected = {
+        "echo.desktop",
+        "AppRun",
+        "usr",
+        "usr/bin",
+        "usr/bin/echo",
+        "usr/share",
+        "usr/share/applications",
+        "usr/share/applications/echo.desktop",
+        "usr/share/applications",
+        "usr/share",
+        "usr",
+        "utilities-terminal.svg"
+    };
+
+    for (const auto& file: appImage.files())
+        expected.erase(file);
+
+    ASSERT_TRUE(expected.empty());
+}
+
+TEST_F(AppImageTests, type1ExtractFile) {
+    auto tmpFilePath = getTmpFilePath();
+
+    core::AppImage appImage(TEST_DATA_DIR "/AppImageExtract_6-x86_64.AppImage");
+    auto fItr = appImage.files().begin();
+    while (fItr != fItr.end() && *fItr != "AppImageExtract.desktop")
+        ++fItr;
+    std::cout << "Extracting " << *fItr << " to " << tmpFilePath << std::endl;
+    fItr.extractTo(tmpFilePath);
+    ASSERT_TRUE(fileSize(tmpFilePath) > 0);
+
+    remove(tmpFilePath.c_str());
+}
+
+TEST_F(AppImageTests, type2ExtractFile) {
+    auto tmpFilePath = getTmpFilePath();
+
+    core::AppImage appImage(TEST_DATA_DIR "/Echo-x86_64.AppImage");
+    auto fItr = appImage.files().begin();
+    while (fItr != fItr.end() && *fItr != "usr/share/applications/echo.desktop")
+        ++fItr;
+    std::cout << "Extracting " << *fItr << " to " << tmpFilePath << std::endl;
+    fItr.extractTo(tmpFilePath);
+    ASSERT_TRUE(fileSize(tmpFilePath) > 0);
+
+    remove(tmpFilePath.c_str());
+}
+
+TEST_F(AppImageTests, type1ReadFile) {
+    core::AppImage appImage(TEST_DATA_DIR "/AppImageExtract_6-x86_64.AppImage");
+    auto fItr = appImage.files().begin();
+    std::vector<char> desktopData;
+    std::vector<char> iconData;
+
+    while (fItr != fItr.end()) {
+        if (*fItr == "AppImageExtract.desktop")
+            desktopData.assign(std::istreambuf_iterator<char>(fItr.read()), std::istreambuf_iterator<char>());
+
+        if (*fItr == ".DirIcon")
+            iconData.assign(std::istreambuf_iterator<char>(fItr.read()), std::istreambuf_iterator<char>());
+        ++fItr;
+    }
+
+    ASSERT_FALSE(desktopData.empty());
+    ASSERT_FALSE(iconData.empty());
+}
+
+TEST_F(AppImageTests, type2ReadFile) {
+    core::AppImage appImage(TEST_DATA_DIR "/Echo-x86_64.AppImage");
+    auto fItr = appImage.files().begin();
+    std::vector<char> desktopData;
+    std::vector<char> iconData;
+
+    while (fItr != fItr.end()) {
+        if (*fItr == "usr/share/applications/echo.desktop")
+            desktopData.assign(std::istreambuf_iterator<char>(fItr.read()), std::istreambuf_iterator<char>());
+
+        if (*fItr == "utilities-terminal.svg")
+            iconData.assign(std::istreambuf_iterator<char>(fItr.read()), std::istreambuf_iterator<char>());
+        ++fItr;
+    }
+
+    ASSERT_FALSE(desktopData.empty());
+    ASSERT_FALSE(iconData.empty());
+}
+
+TEST_F(AppImageTests, extractEntryMultipleTimes) {
+    auto tmpPath = bf::temp_directory_path() / bf::unique_path();
+
+    core::AppImage appImage(TEST_DATA_DIR "/Echo-x86_64.AppImage");
+    auto itr = appImage.files();
+    // Extract two times
+
+    ASSERT_NO_THROW(itr.extractTo(tmpPath.string()));
+    ASSERT_THROW(itr.extractTo(tmpPath.string()), core::PayloadIteratorError);
+    bf::remove(tmpPath);
+
+    // Extract and read
+    itr = appImage.files();
+    ASSERT_NO_THROW(itr.extractTo(tmpPath.string()));
+    ASSERT_THROW(itr.read(), core::PayloadIteratorError);
+    bf::remove(tmpPath);
+
+    // Read two times
+    itr = appImage.files();
+    ASSERT_NO_THROW(std::string(std::istream_iterator<char>(itr.read()), std::istream_iterator<char>()));
+    ASSERT_THROW(itr.read(), core::PayloadIteratorError);
+
+    // Read and extract
+    itr = appImage.files();
+    ASSERT_NO_THROW(std::string(std::istream_iterator<char>(itr.read()), std::istream_iterator<char>()));
+    ASSERT_THROW(itr.extractTo(tmpPath.string()), core::PayloadIteratorError);
+    bf::remove(tmpPath);
+}
diff --git a/tests/libappimage/core/impl/TestTraversalType1.cpp b/tests/libappimage/core/impl/TestTraversalType1.cpp
new file mode 100644 (file)
index 0000000..2d88279
--- /dev/null
@@ -0,0 +1,116 @@
+// system
+#include <fstream>
+
+// library
+#include <gtest/gtest.h>
+#include <boost/filesystem.hpp>
+
+// local
+#include <appimage/core/exceptions.h>
+#include <core/impl/TraversalType1.h>
+
+
+using namespace appimage::core;
+using namespace appimage::core::impl;
+
+class TestTraversalType1 : public ::testing::Test {
+protected:
+    TraversalType1 traversal;
+public:
+    TestTraversalType1() : traversal(TEST_DATA_DIR "/AppImageExtract_6-x86_64.AppImage") {}
+};
+
+TEST_F(TestTraversalType1, traversal) {
+    ASSERT_FALSE(traversal.isCompleted());
+
+    std::map<std::string, PayloadEntryType> expectedEntries = {
+        std::make_pair("usr", PayloadEntryType::DIR),
+        std::make_pair("usr/lib", PayloadEntryType::DIR),
+        std::make_pair("usr/bin", PayloadEntryType::DIR),
+        std::make_pair("AppRun", PayloadEntryType::REGULAR),
+        std::make_pair("AppImageExtract.desktop", PayloadEntryType::REGULAR),
+        std::make_pair(".DirIcon", PayloadEntryType::REGULAR),
+        std::make_pair("AppImageExtract.png", PayloadEntryType::LINK),
+        std::make_pair("usr/bin/appimageextract", PayloadEntryType::REGULAR),
+        std::make_pair("usr/lib/libisoburn.so.1", PayloadEntryType::REGULAR),
+        std::make_pair("usr/bin/xorriso", PayloadEntryType::REGULAR),
+        std::make_pair("usr/lib/libburn.so.4", PayloadEntryType::REGULAR),
+        std::make_pair("usr/lib/libisofs.so.6", PayloadEntryType::REGULAR),
+    };
+
+    while (!traversal.isCompleted()) {
+        const auto entryName = traversal.getEntryPath();
+        std::cerr << entryName << std::endl;
+        auto itr = expectedEntries.find(entryName);
+        ASSERT_NE(itr, expectedEntries.end());
+        ASSERT_EQ(itr->second, traversal.getEntryType());
+
+        expectedEntries.erase(entryName);
+
+        ASSERT_NO_THROW(traversal.next());
+    }
+
+    ASSERT_TRUE(expectedEntries.empty());
+}
+
+TEST_F(TestTraversalType1, extract) {
+    auto tmpPath = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path();
+
+    while (!traversal.isCompleted()) {
+        if (traversal.getEntryPath() == "AppImageExtract.desktop") {
+            traversal.extract(tmpPath.string());
+            ASSERT_TRUE(boost::filesystem::file_size(tmpPath) > 0);
+            break;
+        }
+
+        traversal.next();
+    }
+
+    boost::filesystem::remove_all(tmpPath);
+}
+
+TEST_F(TestTraversalType1, read) {
+    std::string expected = "[Desktop Entry]\n"
+                           "Name=AppImageExtract\n"
+                           "Exec=appimageextract\n"
+                           "Icon=AppImageExtract\n"
+                           "Terminal=true\n"
+                           "Type=Application\n"
+                           "Categories=Development;\n"
+                           "Comment=Extract AppImage contents, part of AppImageKit\n"
+                           "StartupNotify=true\n";
+
+    while (!traversal.isCompleted()) {
+        if (traversal.getEntryPath() == "AppImageExtract.desktop") {
+            std::string content{std::istreambuf_iterator<char>(traversal.read()),
+                                std::istreambuf_iterator<char>()};
+
+            ASSERT_EQ(expected, content);
+
+            // Try re-read a given entry
+            content = std::string{std::istreambuf_iterator<char>(traversal.read()),
+                                  std::istreambuf_iterator<char>()};
+
+            // As entries can be read only once an empty string is expected
+            ASSERT_EQ("", content);
+            break;
+        }
+
+        traversal.next();
+    }
+}
+
+TEST_F(TestTraversalType1, getEntryLink) {
+    auto tmpPath = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path();
+
+    while (!traversal.isCompleted()) {
+        if (traversal.getEntryPath() == "AppImageExtract.png") {
+            ASSERT_EQ(traversal.getEntryLinkTarget(), ".DirIcon");
+            break;
+        }
+
+        traversal.next();
+    }
+
+    boost::filesystem::remove_all(tmpPath);
+}
diff --git a/tests/libappimage/core/impl/TestTraversalType2.cpp b/tests/libappimage/core/impl/TestTraversalType2.cpp
new file mode 100644 (file)
index 0000000..04bd172
--- /dev/null
@@ -0,0 +1,118 @@
+// system
+#include <fstream>
+
+// library
+#include <gtest/gtest.h>
+#include <boost/filesystem.hpp>
+
+// local
+#include <appimage/core/exceptions.h>
+#include <core/impl/TraversalType2.h>
+
+
+using namespace appimage::core;
+using namespace appimage::core::impl;
+
+class TestTraversalType2 : public ::testing::Test {
+protected:
+    TraversalType2 traversal;
+public:
+    TestTraversalType2() : traversal(TEST_DATA_DIR "/Echo-x86_64.AppImage") {}
+};
+
+TEST_F(TestTraversalType2, traversal) {
+    ASSERT_FALSE(traversal.isCompleted());
+
+    std::vector<std::pair<std::string, PayloadEntryType>> expectedEntries = {
+        std::make_pair(".DirIcon", PayloadEntryType::LINK),
+        std::make_pair("AppRun", PayloadEntryType::REGULAR),
+        std::make_pair("echo.desktop", PayloadEntryType::LINK),
+        std::make_pair("usr", PayloadEntryType::DIR),
+        std::make_pair("usr/bin", PayloadEntryType::DIR),
+        std::make_pair("usr/bin/echo", PayloadEntryType::REGULAR),
+        std::make_pair("usr/bin", PayloadEntryType::DIR),
+        std::make_pair("usr/share", PayloadEntryType::DIR),
+        std::make_pair("usr/share/applications", PayloadEntryType::DIR),
+        std::make_pair("usr/share/applications/echo.desktop", PayloadEntryType::REGULAR),
+        std::make_pair("usr/share/applications", PayloadEntryType::DIR),
+        std::make_pair("usr/share", PayloadEntryType::DIR),
+        std::make_pair("usr", PayloadEntryType::DIR),
+        std::make_pair("utilities-terminal.svg", PayloadEntryType::REGULAR),
+    };
+
+    while (!traversal.isCompleted()) {
+        auto entry = std::make_pair(traversal.getEntryPath(), traversal.getEntryType());
+
+        auto itr = std::find(expectedEntries.begin(), expectedEntries.end(), entry);
+        ASSERT_NE(itr, expectedEntries.end());
+
+        expectedEntries.erase(itr);
+
+        ASSERT_NO_THROW(traversal.next());
+    }
+
+    ASSERT_TRUE(expectedEntries.empty());
+}
+
+TEST_F(TestTraversalType2, extract) {
+    auto tmpPath = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path();
+
+    while (!traversal.isCompleted()) {
+
+        // Extract Synlink
+        if (traversal.getEntryPath() == ".DirIcon") {
+            traversal.extract(tmpPath.string());
+
+            ASSERT_TRUE(boost::filesystem::is_symlink(tmpPath));
+
+            auto synlinkTarget = boost::filesystem::read_symlink(tmpPath);
+            ASSERT_EQ(synlinkTarget, boost::filesystem::path("utilities-terminal.svg"));
+
+            boost::filesystem::remove_all(tmpPath);
+        }
+
+        // Extract Dir
+        if (traversal.getEntryPath() == "usr") {
+            traversal.extract(tmpPath.string());
+
+            ASSERT_TRUE(boost::filesystem::is_directory(tmpPath));
+        }
+
+        traversal.next();
+    }
+}
+
+TEST_F(TestTraversalType2, read) {
+    std::string expected = "[Desktop Entry]\n"
+                           "Version=1.0\n"
+                           "Type=Application\n"
+                           "Name=Echo\n"
+                           "Comment=Just echo.\n"
+                           "Exec=echo %F\n"
+                           "Icon=utilities-terminal\n";
+
+    while (!traversal.isCompleted()) {
+        if (traversal.getEntryPath() == "usr/share/applications/echo.desktop") {
+            std::string content{std::istreambuf_iterator<char>(traversal.read()),
+                                std::istreambuf_iterator<char>()};
+
+            ASSERT_EQ(expected, content);
+            break;
+        }
+
+        traversal.next();
+    }
+}
+
+
+TEST_F(TestTraversalType2, getEntryLink) {
+    auto tmpPath = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path();
+
+    while (!traversal.isCompleted()) {
+        // Extract Synlink
+        if (traversal.getEntryPath() == ".DirIcon")
+            ASSERT_EQ(traversal.getEntryLinkTarget(), "utilities-terminal.svg");
+
+        traversal.next();
+    }
+}
diff --git a/tests/libappimage/desktop_integration/CMakeLists.txt b/tests/libappimage/desktop_integration/CMakeLists.txt
new file mode 100644 (file)
index 0000000..a5296f7
--- /dev/null
@@ -0,0 +1,48 @@
+set(
+    TestDesktopIntegrationSources
+
+    TestIntegrationManager.cpp
+
+    integrator/TestDesktopIntegration.cpp
+    integrator/TestDesktopEntryEditor.cpp
+
+    $<TARGET_OBJECTS:core>
+    $<TARGET_OBJECTS:appimage_utils>
+    $<TARGET_OBJECTS:appimage_desktop_integration>
+)
+
+if(LIBAPPIMAGE_THUMBNAILER_ENABLED)
+    set(TestDesktopIntegrationSources ${TestDesktopIntegrationSources} TestThumbnailer.cpp)
+endif()
+
+add_executable(TestDesktopIntegration ${TestDesktopIntegrationSources})
+
+target_include_directories(
+    TestDesktopIntegration
+    PRIVATE "${PROJECT_SOURCE_DIR}/include"
+    PRIVATE "${PROJECT_SOURCE_DIR}/src/libappimage"
+    PRIVATE "${PROJECT_SOURCE_DIR}/src/libappimage/desktop_integration"
+    PRIVATE "$<BUILD_INTERFACE:${PROJECT_BINARY_DIR}/generated-headers>"
+)
+
+target_link_libraries(
+    TestDesktopIntegration
+    PRIVATE libappimage_shared
+    PRIVATE libsquashfuse
+    PRIVATE libarchive
+    PRIVATE xz
+    PRIVATE libzlib
+    PRIVATE XdgUtils::DesktopEntry
+    PRIVATE XdgUtils::BaseDir
+    PRIVATE Boost::filesystem
+    # Builds using old glib versions requiered that the glib to be initialized, this initialization is performed
+    # statically once glib is loaded.
+    PRIVATE libglib
+    PUBLIC dl
+    PRIVATE gtest
+    PRIVATE gtest_main
+    PRIVATE librsvg
+    PRIVATE libcairo
+)
+
+add_test(TestDesktopIntegration TestDesktopIntegration)
diff --git a/tests/libappimage/desktop_integration/TestIntegrationManager.cpp b/tests/libappimage/desktop_integration/TestIntegrationManager.cpp
new file mode 100644 (file)
index 0000000..5476221
--- /dev/null
@@ -0,0 +1,111 @@
+// system
+#include <sstream>
+
+// library headers
+#include <gtest/gtest.h>
+#include <boost/filesystem.hpp>
+
+// local
+#include "appimage/desktop_integration/exceptions.h"
+#include "appimage/desktop_integration/IntegrationManager.h"
+#include "utils/hashlib.h"
+#include "utils/path_utils.h"
+
+using namespace appimage::desktop_integration;
+namespace bf = boost::filesystem;
+
+class TestIntegrationManager : public ::testing::Test {
+protected:
+    bf::path userDirPath;
+
+    void SetUp() override {
+        userDirPath = bf::temp_directory_path() / boost::filesystem::unique_path();
+        bf::create_directories(userDirPath);
+
+        ASSERT_FALSE(userDirPath.empty());
+    }
+
+    void TearDown() override {
+        bf::remove_all(userDirPath);
+    }
+
+    void createStubFile(const bf::path& path, const std::string& content = "") {
+        bf::create_directories(path.parent_path());
+        bf::ofstream f(path);
+        f << content;
+    }
+};
+
+TEST_F(TestIntegrationManager, registerAppImage) {
+    std::string appImagePath = TEST_DATA_DIR "Echo-x86_64.AppImage";
+    IntegrationManager manager(userDirPath.string());
+    appimage::core::AppImage appImage(appImagePath);
+    manager.registerAppImage(appImage);
+
+    std::string md5 = appimage::utils::hashPath(appImagePath.c_str());
+
+    bf::path expectedDesktopFilePath = userDirPath / ("applications/appimagekit_" + md5 + "-Echo.desktop");
+    ASSERT_TRUE(bf::exists(expectedDesktopFilePath));
+
+    bf::path expectedIconFilePath =
+        userDirPath / ("icons/hicolor/scalable/apps/appimagekit_" + md5 + "_utilities-terminal.svg");
+    ASSERT_TRUE(bf::exists(expectedIconFilePath));
+}
+
+TEST_F(TestIntegrationManager, isARegisteredAppImage) {
+    std::string appImagePath = TEST_DATA_DIR "Echo-x86_64.AppImage";
+    IntegrationManager manager(userDirPath.string());
+
+    ASSERT_FALSE(manager.isARegisteredAppImage(appImagePath));
+
+    { // Generate fake desktop entry file
+        std::string md5 = appimage::utils::hashPath(appImagePath.c_str());
+
+        bf::path desployedDesktopFilePath = userDirPath / ("applications/appimagekit_" + md5 + "-Echo.desktop");
+        createStubFile(desployedDesktopFilePath, "[Desktop Entry]");
+
+        ASSERT_TRUE(bf::exists(desployedDesktopFilePath));
+    }
+
+    ASSERT_TRUE(manager.isARegisteredAppImage(appImagePath));
+}
+
+TEST_F(TestIntegrationManager, shallAppImageBeRegistered) {
+    IntegrationManager manager;
+
+    ASSERT_TRUE(manager.shallAppImageBeRegistered(
+        appimage::core::AppImage(TEST_DATA_DIR "Echo-x86_64.AppImage")));
+    ASSERT_FALSE(manager.shallAppImageBeRegistered(
+        appimage::core::AppImage(TEST_DATA_DIR "Echo-no-integrate-x86_64.AppImage")));
+    ASSERT_THROW(manager.shallAppImageBeRegistered(
+        appimage::core::AppImage(TEST_DATA_DIR "elffile")), appimage::core::AppImageError);
+}
+
+
+TEST_F(TestIntegrationManager, unregisterAppImage) {
+    std::string appImagePath = TEST_DATA_DIR "Echo-x86_64.AppImage";
+    IntegrationManager manager(userDirPath.string());
+
+    // Generate fake desktop entry file
+    std::string md5 = appimage::utils::hashPath(appImagePath.c_str());
+
+    bf::path desployedDesktopFilePath = userDirPath / ("applications/appimagekit_" + md5 + "-Echo.desktop");
+    createStubFile(desployedDesktopFilePath, "[Desktop Entry]");
+    ASSERT_TRUE(bf::exists(desployedDesktopFilePath));
+
+    bf::path desployedIconFilePath = userDirPath /
+                                     ("icons/hicolor/scalable/apps/appimagekit_" + md5 + "_utilities-terminal.svg");
+    createStubFile(desployedIconFilePath, "<?xml");
+    ASSERT_TRUE(bf::exists(desployedIconFilePath));
+
+
+    bf::path desployedMimeTypePackageFilePath = userDirPath / ("mime/packages/appimagekit_" + md5 + "-echo.xml");
+    createStubFile(desployedMimeTypePackageFilePath, "<?xml");
+    ASSERT_TRUE(bf::exists(desployedMimeTypePackageFilePath));
+
+    manager.unregisterAppImage(appImagePath);
+
+    ASSERT_FALSE(bf::exists(desployedDesktopFilePath));
+    ASSERT_FALSE(bf::exists(desployedIconFilePath));
+    ASSERT_FALSE(bf::exists(desployedMimeTypePackageFilePath));
+}
diff --git a/tests/libappimage/desktop_integration/TestThumbnailer.cpp b/tests/libappimage/desktop_integration/TestThumbnailer.cpp
new file mode 100644 (file)
index 0000000..1998d3c
--- /dev/null
@@ -0,0 +1,98 @@
+// system
+#include <sstream>
+
+// library headers
+#include <gtest/gtest.h>
+#include <boost/filesystem.hpp>
+
+// local
+#include "appimage/desktop_integration/exceptions.h"
+#include "Thumbnailer.h"
+#include "utils/path_utils.h"
+
+using namespace appimage::desktop_integration;
+namespace bf = boost::filesystem;
+
+class TestThumbnailer : public ::testing::Test {
+protected:
+    bf::path xdgCacheHome = "";
+
+    void SetUp() override {
+        xdgCacheHome = bf::temp_directory_path() / boost::filesystem::unique_path();
+        bf::create_directories(xdgCacheHome);
+
+        ASSERT_FALSE(xdgCacheHome.empty());
+    }
+
+    void TearDown() override {
+        bf::remove_all(xdgCacheHome);
+    }
+
+    void createStubFile(const bf::path& path, const std::string& content = "") {
+        bf::create_directories(path.parent_path());
+        bf::ofstream f(path);
+        f << content;
+    }
+};
+
+TEST_F(TestThumbnailer, createType1) {
+    std::string appImagePath = TEST_DATA_DIR "AppImageExtract_6-x86_64.AppImage";
+    Thumbnailer thumbnailer(xdgCacheHome.string());
+
+    appimage::core::AppImage appImage{appImagePath};
+    thumbnailer.create(appImage);
+
+    auto canonicalAppImagePath = boost::filesystem::weakly_canonical(appImagePath).string();
+    std::string canonicalPathMd5 = appimage::utils::hashPath(canonicalAppImagePath);
+
+    bf::path normalIconPath = xdgCacheHome / "thumbnails/normal" / (canonicalPathMd5 + ".png");
+    bf::path largeIconPath = xdgCacheHome / "thumbnails/large" / (canonicalPathMd5 + ".png");
+
+    ASSERT_TRUE(bf::exists(normalIconPath));
+    ASSERT_FALSE(bf::is_empty(normalIconPath));
+
+    ASSERT_TRUE(bf::exists(largeIconPath));
+    ASSERT_FALSE(bf::is_empty(largeIconPath));
+}
+
+
+TEST_F(TestThumbnailer, createType2) {
+    std::string appImagePath = TEST_DATA_DIR "Echo-x86_64.AppImage";
+    Thumbnailer thumbnailer(xdgCacheHome.string());
+
+    appimage::core::AppImage appImage{appImagePath};
+    thumbnailer.create(appImage);
+
+    std::string canonicalPathMd5 = appimage::utils::hashPath(appImagePath);
+
+    bf::path normalIconPath = xdgCacheHome / "thumbnails/normal" / (canonicalPathMd5 + ".png");
+    bf::path largeIconPath = xdgCacheHome / "thumbnails/large" / (canonicalPathMd5 + ".png");
+
+    ASSERT_TRUE(bf::exists(normalIconPath));
+    ASSERT_FALSE(bf::is_empty(normalIconPath));
+
+    ASSERT_TRUE(bf::exists(largeIconPath));
+    ASSERT_FALSE(bf::is_empty(largeIconPath));
+}
+
+TEST_F(TestThumbnailer, remove) {
+    std::string appImagePath = TEST_DATA_DIR "Echo-x86_64.AppImage";
+    Thumbnailer thumbnailer(xdgCacheHome.string());
+
+    std::string canonicalPathMd5 = appimage::utils::hashPath(appImagePath);
+
+    bf::path normalIconPath = xdgCacheHome / "thumbnails/normal" / (canonicalPathMd5 + ".png");
+    bf::path largeIconPath = xdgCacheHome / "thumbnails/large" / (canonicalPathMd5 + ".png");
+
+
+    createStubFile(normalIconPath);
+    createStubFile(largeIconPath);
+
+    ASSERT_TRUE(bf::exists(normalIconPath));
+    ASSERT_TRUE(bf::exists(largeIconPath));
+
+    thumbnailer.remove(appImagePath);
+
+    ASSERT_FALSE(bf::exists(normalIconPath));
+    ASSERT_FALSE(bf::exists(largeIconPath));
+}
diff --git a/tests/libappimage/desktop_integration/integrator/TestDesktopEntryEditor.cpp b/tests/libappimage/desktop_integration/integrator/TestDesktopEntryEditor.cpp
new file mode 100644 (file)
index 0000000..55bf533
--- /dev/null
@@ -0,0 +1,112 @@
+//system
+#include <sstream>
+
+// library
+#include <gtest/gtest.h>
+#include <XdgUtils/DesktopEntry/DesktopEntry.h>
+
+// local
+#include <appimage/core/exceptions.h>
+#include "utils/hashlib.h"
+#include "integrator/DesktopEntryEditError.h"
+#include "integrator/DesktopEntryEditor.h"
+
+using namespace appimage::desktop_integration::integrator;
+using namespace appimage::utils;
+
+class DesktopEntryEditorTests : public ::testing::Test {
+protected:
+    std::stringstream originalData;
+protected:
+
+    void SetUp() override {
+        originalData << "[Desktop Entry]\n"
+                     << "Version=1.0\n"
+                     << "Type=Application\n"
+                     << "Name=Foo Viewer\n"
+                     << "Name[es]=Visor de Foo\n"
+                     << "Name[en]=Foo Viewer 0.1.1\n"
+                     << "Comment=The best viewer for Foo objects available!\n"
+                     << "TryExec=fooview\n"
+                     << "Exec=fooview %F\n"
+                     << "Icon=fooview\n"
+                     << "Icon[es]=fooview-es\n"
+                     << "MimeType=image/x-foo;\n"
+                     << "Actions=Gallery;Create;\n"
+                     << "\n"
+                     << "[Desktop Action Gallery]\n"
+                     << "Exec=fooview --gallery\n"
+                     << "Name=Browse Gallery\n"
+                     << "\n"
+                     << "[Desktop Action Create]\n"
+                     << "Exec=fooview --create-new\n"
+                     << "Name=Create a new Foo!\n"
+                     << "Icon=fooview-new";
+    }
+};
+
+TEST_F(DesktopEntryEditorTests, setPath) {
+    XdgUtils::DesktopEntry::DesktopEntry entry(originalData);
+    DesktopEntryEditor editor;
+    std::string path = TEST_DATA_DIR "Echo-x86_64.AppImage";
+    editor.setAppImagePath(path);
+    editor.setIdentifier("uuid");
+    editor.edit(entry);
+
+    ASSERT_EQ(entry.get("Desktop Entry/Exec"), path + " %F");
+    ASSERT_EQ(entry.get("Desktop Entry/TryExec"), path);
+    ASSERT_EQ(entry.get("Desktop Action Gallery/Exec"), path + " --gallery");
+    ASSERT_EQ(entry.get("Desktop Action Create/Exec"), path + " --create-new");
+}
+
+TEST_F(DesktopEntryEditorTests, setIcons) {
+    XdgUtils::DesktopEntry::DesktopEntry entry(originalData);
+    DesktopEntryEditor editor;
+    std::string path = TEST_DATA_DIR "Echo-x86_64.AppImage";
+
+    editor.setVendorPrefix("test");
+
+    std::string appImagePathMd5 = hashlib::toHex(hashlib::md5(path));
+    editor.setIdentifier(appImagePathMd5);
+    editor.edit(entry);
+
+    ASSERT_EQ(entry.get("Desktop Entry/Icon"), "test_" + appImagePathMd5 + "_fooview");
+    ASSERT_EQ(entry.get("Desktop Entry/Icon[es]"), "test_" + appImagePathMd5 + "_fooview-es");
+    ASSERT_EQ(entry.get("Desktop Action Create/Icon"), "test_" + appImagePathMd5 + "_fooview-new");
+
+    ASSERT_EQ(entry.get("Desktop Entry/X-AppImage-Old-Icon"), "fooview");
+    ASSERT_EQ(entry.get("Desktop Entry/X-AppImage-Old-Icon[es]"), "fooview-es");
+    ASSERT_EQ(entry.get("Desktop Action Create/X-AppImage-Old-Icon"), "fooview-new");
+}
+
+TEST_F(DesktopEntryEditorTests, setVersion) {
+    XdgUtils::DesktopEntry::DesktopEntry entry(originalData);
+    DesktopEntryEditor editor;
+    std::string path = TEST_DATA_DIR "Echo-x86_64.AppImage";
+
+    editor.setVendorPrefix("prefix");
+    editor.setIdentifier("uuid");
+
+    editor.setAppImageVersion("0.1.1");
+    editor.edit(entry);
+
+    ASSERT_EQ(entry.get("Desktop Entry/Name"), "Foo Viewer (0.1.1)");
+    ASSERT_EQ(entry.get("Desktop Entry/Name[en]"), "Foo Viewer 0.1.1");
+    ASSERT_EQ(entry.get("Desktop Entry/Name[es]"), "Visor de Foo (0.1.1)");
+
+    ASSERT_EQ(entry.get("Desktop Entry/X-AppImage-Old-Name"), "Foo Viewer");
+    ASSERT_FALSE(entry.exists("Desktop Entry/X-AppImage-Old-Name[en]"));
+    ASSERT_EQ(entry.get("Desktop Entry/X-AppImage-Old-Name[es]"), "Visor de Foo");
+}
+
+TEST_F(DesktopEntryEditorTests, setIdentifier) {
+    XdgUtils::DesktopEntry::DesktopEntry entry(originalData);
+
+    DesktopEntryEditor editor;
+    editor.setVendorPrefix("prefix");
+    editor.setIdentifier("uuid");
+    editor.setAppImageVersion("0.1.1");
+    editor.edit(entry);
+
+    ASSERT_EQ(entry.get("Desktop Entry/X-AppImage-Identifier"), "uuid");
+}
diff --git a/tests/libappimage/desktop_integration/integrator/TestDesktopIntegration.cpp b/tests/libappimage/desktop_integration/integrator/TestDesktopIntegration.cpp
new file mode 100644 (file)
index 0000000..ab52417
--- /dev/null
@@ -0,0 +1,86 @@
+// system
+#include <sstream>
+
+// library headers
+#include <gtest/gtest.h>
+#include <boost/filesystem.hpp>
+#include <XdgUtils/DesktopEntry/DesktopEntry.h>
+
+// local
+#include "appimage/desktop_integration/exceptions.h"
+#include "integrator/Integrator.h"
+#include "utils/hashlib.h"
+#include "utils/path_utils.h"
+
+using namespace appimage::desktop_integration::integrator;
+namespace bf = boost::filesystem;
+
+class DesktopIntegrationTests : public ::testing::Test {
+protected:
+    bf::path userDirPath;
+
+    void SetUp() override {
+        userDirPath = bf::temp_directory_path() / boost::filesystem::unique_path();
+        bf::create_directories(userDirPath);
+
+        ASSERT_FALSE(userDirPath.empty());
+    }
+
+    void TearDown() override {
+        bf::remove_all(userDirPath);
+    }
+
+};
+
+TEST_F(DesktopIntegrationTests, integrateEchoAppImage) {
+    std::string appImagePath = TEST_DATA_DIR "Echo-x86_64.AppImage";
+    appimage::core::AppImage appImage(appImagePath);
+    Integrator i(appImage, userDirPath.string());
+
+    i.integrate();
+
+    std::string md5 = appimage::utils::hashPath(appImagePath.c_str());
+
+    bf::path expectedDesktopFilePath = userDirPath / ("applications/appimagekit_" + md5 + "-Echo.desktop");
+    ASSERT_TRUE(bf::exists(expectedDesktopFilePath));
+
+    bf::path expectedIconFilePath =
+        userDirPath / ("icons/hicolor/scalable/apps/appimagekit_" + md5 + "_utilities-terminal.svg");
+    ASSERT_TRUE(bf::exists(expectedIconFilePath));
+}
+
+TEST_F(DesktopIntegrationTests, integrateAppImageExtract) {
+    std::string appImagePath = TEST_DATA_DIR "AppImageExtract_6-x86_64.AppImage";
+    appimage::core::AppImage appImage(appImagePath);
+    Integrator i(appImage, userDirPath.string());
+
+    i.integrate();
+
+    std::string md5 = appimage::utils::hashPath(appImagePath.c_str());
+
+    bf::path expectedDesktopFilePath = userDirPath / ("applications/appimagekit_" + md5 + "-AppImageExtract.desktop");
+    ASSERT_TRUE(bf::exists(expectedDesktopFilePath));
+
+    bf::path expectedIconFilePath =
+        userDirPath / ("icons/hicolor/48x48/apps/appimagekit_" + md5 + "_AppImageExtract.png");
+    ASSERT_TRUE(bf::exists(expectedIconFilePath));
+}
+
+TEST_F(DesktopIntegrationTests, integrateEchoNoIntegrate) {
+    appimage::core::AppImage appImage(TEST_DATA_DIR "Echo-no-integrate-x86_64.AppImage");
+    Integrator i(appImage, userDirPath.string());
+
+    ASSERT_THROW(i.integrate(), appimage::desktop_integration::DesktopIntegrationError);
+}
+
+TEST_F(DesktopIntegrationTests, emtpyXdgDataDir) {
+    appimage::core::AppImage appImage(TEST_DATA_DIR "Echo-no-integrate-x86_64.AppImage");
+
+    ASSERT_THROW(Integrator(appImage, ""), appimage::desktop_integration::DesktopIntegrationError);
+}
+
+TEST_F(DesktopIntegrationTests, malformedDesktopEntry) {
+    appimage::core::AppImage appImage(TEST_DATA_DIR "broken-desktop-file-x86_64.AppImage");
+
+    ASSERT_THROW(Integrator(appImage, userDirPath.string()), appimage::desktop_integration::DesktopIntegrationError);
+}
diff --git a/tests/libappimage/legacy/CMakeLists.txt b/tests/libappimage/legacy/CMakeLists.txt
new file mode 100644 (file)
index 0000000..874d92d
--- /dev/null
@@ -0,0 +1,19 @@
+add_library(fixtures INTERFACE)
+target_sources(fixtures INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/fixtures.h)
+set_property(TARGET fixtures PROPERTY INTERFACE_LINK_LIBRARIES xdg-basedir gtest)
+
+if (NOT LIBAPPIMAGE_SHARED_ONLY)
+    add_executable(test_libappimage test_libappimage.cpp)
+    target_link_libraries(test_libappimage fixtures libappimage libsquashfuse libglib libgobject gtest gtest_main)
+
+    add_executable(test-xdg-basedir test-xdg-basedir.cpp)
+    target_link_libraries(test-xdg-basedir fixtures xdg-basedir)
+    add_test(test-xdg-basedir test-xdg-basedir)
+    add_test(test_libappimage test_libappimage)
+endif()
+
+add_executable(test_shared test_shared.cpp)
+target_link_libraries(test_shared fixtures libappimage_shared)
+add_test(test_shared test_shared)
+# needed for some const->non-const conversions
+target_compile_options(test_shared PRIVATE -fpermissive)
diff --git a/tests/libappimage/legacy/fixtures.h b/tests/libappimage/legacy/fixtures.h
new file mode 100644 (file)
index 0000000..dc40832
--- /dev/null
@@ -0,0 +1,191 @@
+#pragma once
+
+#include <cerrno>
+#include <ftw.h>
+#include <unistd.h>
+#include <xdg-basedir.h>
+#include <gtest/gtest.h>
+
+
+// fixture providing a temporary directory, and a temporary home directory within that directory
+// overwrites HOME environment variable to ensure desktop files etc. are not installed in the system
+class TestBase : public ::testing::Test {
+private:
+    char* oldHome;
+    char* oldXdgDataHome;
+    char* oldXdgConfigHome;
+
+public:
+    std::string tempDir;
+    std::string tempHome;
+
+protected:
+    std::string elf_file_path;
+    std::string iso_9660_file_path;
+    std::string appImage_type_1_file_path;
+    std::string appImage_type_1_no_magic_file_path;
+    std::string appImage_type_2_file_path;
+    std::string appImage_type_2_versioned_path;
+    std::string appImage_type_2_terminal_file_path;
+    std::string appImage_type_2_shall_not_integrate_path;
+
+public:
+    TestBase() {
+        char* tmpl = strdup("/tmp/AppImageKit-unit-tests-XXXXXX");
+        tempDir = mkdtemp(tmpl);
+        free(tmpl);
+
+        tempHome = tempDir + "/HOME";
+
+        mkdir(tempHome.c_str(), 0700);
+
+        oldHome = getenv("HOME");
+        oldXdgDataHome = getenv("XDG_DATA_HOME");
+        oldXdgConfigHome = getenv("XDG_CONFIG_HOME");
+
+        std::string newXdgDataHome = tempHome + "/.local/share";
+        std::string newXdgConfigHome = tempHome + "/.config";
+
+        setenv("HOME", tempHome.c_str(), true);
+        setenv("XDG_DATA_HOME", newXdgDataHome.c_str(), true);
+        setenv("XDG_CONFIG_HOME", newXdgConfigHome.c_str(), true);
+
+        char* xdgDataHome = xdg_data_home();
+        char* xdgConfigHome = xdg_config_home();
+
+        EXPECT_EQ(getenv("HOME"), tempHome);
+        EXPECT_EQ(newXdgDataHome, xdgDataHome);
+        EXPECT_EQ(newXdgConfigHome, xdgConfigHome);
+
+        free(xdgDataHome);
+        free(xdgConfigHome);
+
+        iso_9660_file_path = std::string(TEST_DATA_DIR) + "/minimal.iso";
+        elf_file_path = std::string(TEST_DATA_DIR) + "/elffile";
+        appImage_type_1_file_path = std::string(TEST_DATA_DIR) + "/AppImageExtract_6-x86_64.AppImage";
+        appImage_type_1_no_magic_file_path = std::string(TEST_DATA_DIR) + "/AppImageExtract_6_no_magic_bytes-x86_64.AppImage";
+        appImage_type_2_file_path = std::string(TEST_DATA_DIR) + "/Echo-x86_64.AppImage";
+        appImage_type_2_versioned_path = std::string(TEST_DATA_DIR) + "/Echo-test1234-x86_64.AppImage";
+        appImage_type_2_shall_not_integrate_path = std::string(TEST_DATA_DIR) + "/Echo-no-integrate-x86_64.AppImage";
+        appImage_type_2_terminal_file_path = std::string(TEST_DATA_DIR) + "/appimagetool-x86_64.AppImage";
+
+        EXPECT_TRUE(isFile(appImage_type_1_file_path));
+        EXPECT_TRUE(isFile(appImage_type_2_file_path));
+        EXPECT_TRUE(isFile(appImage_type_2_versioned_path));
+        EXPECT_TRUE(isFile(appImage_type_2_terminal_file_path));
+        EXPECT_TRUE(isFile(appImage_type_2_shall_not_integrate_path));
+    };
+
+    ~TestBase() {
+        if (isDir(tempDir)) {
+            rmTree(tempDir);
+        }
+
+        if (oldHome != NULL) {
+            setenv("HOME", oldHome, true);
+        } else {
+            unsetenv("HOME");
+        }
+
+        if (oldXdgDataHome != NULL) {
+            setenv("XDG_DATA_HOME", oldXdgDataHome, true);
+        } else {
+            unsetenv("XDG_DATA_HOME");
+        }
+
+        if (oldXdgConfigHome != NULL) {
+            setenv("XDG_CONFIG_HOME", oldXdgConfigHome, true);
+        } else {
+            unsetenv("XDG_CONFIG_HOME");
+        }
+    }
+
+private:
+    static const int rmTree(const std::string& path) {
+        int rv = nftw(path.c_str(), unlinkCb, 64, FTW_DEPTH|FTW_MOUNT|FTW_PHYS);
+
+        if (rv != 0) {
+            int error = errno;
+            std::cerr << "nftw() in rmTree(" << path << ") failed: " << strerror(error) << std::endl;
+            return rv;
+        }
+
+        return 0;
+    }
+
+    static int unlinkCb(const char* fpath, const struct stat* sb, int typeflag, struct FTW* ftwbuf) {
+        int rv;
+
+        switch (typeflag) {
+            case FTW_D:
+            case FTW_DNR:
+            case FTW_DP:
+                rv = rmdir(fpath);
+                break;
+            default:
+                rv = unlink(fpath);
+                break;
+        }
+
+        return rv;
+    };
+
+public:
+    static const bool isFile(const std::string& path) {
+        struct stat st;
+
+        if (stat(path.c_str(), &st) != 0) {
+            perror("Failed to call stat(): ");
+            return false;
+        }
+
+        return S_ISREG(st.st_mode);
+    }
+
+    static const bool isDir(const std::string& path) {
+        struct stat st;
+
+        if (stat(path.c_str(), &st) != 0) {
+            perror("Failed to call stat(): ");
+            return false;
+        }
+
+        return S_ISDIR(st.st_mode);
+    }
+
+    static const std::vector<std::string> splitString(const std::string& s, char delim = ' ') {
+        std::vector<std::string> result;
+
+        std::stringstream ss(s);
+        std::string item;
+
+        while (std::getline(ss, item, delim)) {
+            result.push_back(item);
+        }
+
+        return result;
+    }
+
+    static const bool isEmptyString(const std::string& str) {
+        // check whether string is empty beforehand, as the string is interpreted as C string and contains a trailing \0
+        if (str.empty())
+            return true;
+
+        for (int i = 0; i < str.length(); i++) {
+            char chr = str[i];
+            if (chr != ' ' && chr != '\t')
+                return false;
+        }
+
+        return true;
+    }
+
+    static const bool stringStartsWith(const std::string& str, const std::string& prefix) {
+        for (int i = 0; i < prefix.length(); i++) {
+            if (str[i] != prefix[i])
+                return false;
+        }
+
+        return true;
+    }
+};
diff --git a/tests/libappimage/legacy/test-xdg-basedir.cpp b/tests/libappimage/legacy/test-xdg-basedir.cpp
new file mode 100644 (file)
index 0000000..4ca3647
--- /dev/null
@@ -0,0 +1,166 @@
+// system headers
+#include <climits>
+#include <string>
+
+// library headers
+#include <gtest/gtest.h>
+
+// local headers
+#include "xdg-basedir.h"
+
+bool compareStrings(const char* str1, const char* str2) {
+    if (str1 == NULL || str2 == NULL)
+        return false;
+
+    return strcmp(str1, str2) == 0;
+}
+
+TEST(xdg_basedir_test, test_user_home_default_value) {
+    char* home = user_home();
+    EXPECT_PRED2(compareStrings, home, getenv("HOME"));
+    free(home);
+}
+
+TEST(xdg_basedir_test, test_user_home_custom_value) {
+    char* oldValue = strdup(getenv("HOME"));
+    setenv("HOME", "ABCDEFG", true);
+
+    char* currentValue = user_home();
+    EXPECT_PRED2(compareStrings, currentValue, getenv("HOME"));
+    EXPECT_PRED2(compareStrings, currentValue, "ABCDEFG");
+    free(currentValue);
+
+    setenv("HOME", oldValue, true);
+    free(oldValue);
+}
+
+TEST(xdg_basedir_test, test_xdg_data_home_default_value) {
+    // make sure env var is not set, to force function to use default value
+    char* oldValue;
+
+    if ((oldValue = getenv("XDG_DATA_HOME")) != NULL) {
+        unsetenv("XDG_DATA_HOME");
+    }
+
+    char* currentValue = xdg_data_home();
+
+    // too lazy to calculate size
+    char* expectedValue = static_cast<char*>(malloc(PATH_MAX));
+    strcpy(expectedValue, getenv("HOME"));
+    strcat(expectedValue, "/.local/share");
+
+    EXPECT_PRED2(compareStrings, currentValue, expectedValue);
+
+    free(expectedValue);
+    free(currentValue);
+
+    if (oldValue != NULL) {
+        setenv("XDG_DATA_HOME", oldValue, true);
+        free(oldValue);
+    }
+}
+
+TEST(xdg_basedir_test, test_xdg_data_home_custom_value) {
+    char* oldValue = getenv("XDG_DATA_HOME");
+    setenv("XDG_DATA_HOME", "HIJKLM", true);
+
+    char* currentValue = xdg_data_home();
+    EXPECT_PRED2(compareStrings, currentValue, "HIJKLM");
+    free(currentValue);
+
+    if (oldValue != NULL) {
+        setenv("XDG_DATA_HOME", oldValue, true);
+        free(oldValue);
+    } else {
+        unsetenv("XDG_DATA_HOME");
+    }
+}
+
+TEST(xdg_basedir_test, test_xdg_config_home_default_value) {
+    // make sure env var is not set, to force function to use default value
+    char* oldValue;
+
+    if ((oldValue = getenv("XDG_CONFIG_HOME")) != NULL) {
+        unsetenv("XDG_CONFIG_HOME");
+    }
+
+    char* currentValue = xdg_config_home();
+
+    // too lazy to calculate size
+    char* expectedValue = static_cast<char*>(malloc(PATH_MAX));
+    strcpy(expectedValue, getenv("HOME"));
+    strcat(expectedValue, "/.config");
+
+    EXPECT_PRED2(compareStrings, currentValue, expectedValue);
+
+    free(expectedValue);
+    free(currentValue);
+
+    if (oldValue != NULL) {
+        setenv("XDG_CONFIG_HOME", oldValue, true);
+        free(oldValue);
+    }
+}
+
+TEST(xdg_basedir_test, test_xdg_config_home_custom_value) {
+    char* oldValue = getenv("XDG_CONFIG_HOME");
+    setenv("XDG_CONFIG_HOME", "NOPQRS", true);
+
+    char* currentValue = xdg_config_home();
+    EXPECT_PRED2(compareStrings, currentValue, "NOPQRS");
+    free(currentValue);
+
+    if (oldValue != NULL) {
+        setenv("XDG_CONFIG_HOME", oldValue, true);
+        free(oldValue);
+    } else {
+        unsetenv("XDG_CONFIG_HOME");
+    }
+}
+
+TEST(xdg_basedir_test, test_xdg_cache_home_default_value) {
+    // make sure env var is not set, to force function to use default value
+    char* oldValue;
+
+    if ((oldValue = getenv("XDG_CACHE_HOME")) != NULL) {
+        unsetenv("XDG_CACHE_HOME");
+    }
+
+    char* currentValue = xdg_cache_home();
+
+    // too lazy to calculate size
+    char* expectedValue = static_cast<char*>(malloc(PATH_MAX));
+    strcpy(expectedValue, getenv("HOME"));
+    strcat(expectedValue, "/.cache");
+
+    EXPECT_PRED2(compareStrings, currentValue, expectedValue);
+
+    free(expectedValue);
+    free(currentValue);
+
+    if (oldValue != NULL) {
+        setenv("XDG_CACHE_HOME", oldValue, true);
+        free(oldValue);
+    }
+}
+
+TEST(xdg_basedir_test, test_xdg_cache_home_custom_value) {
+    char* oldValue = getenv("XDG_CACHE_HOME");
+    setenv("XDG_CACHE_HOME", "TUVWXY", true);
+
+    char* currentValue = xdg_cache_home();
+    EXPECT_PRED2(compareStrings, currentValue, "TUVWXY");
+    free(currentValue);
+
+    if (oldValue != NULL) {
+        setenv("XDG_CACHE_HOME", oldValue, true);
+        free(oldValue);
+    } else {
+        unsetenv("XDG_CACHE_HOME");
+    }
+}
+
+int main(int argc, char **argv) {
+    ::testing::InitGoogleTest(&argc, argv);
+    return RUN_ALL_TESTS();
+}
diff --git a/tests/libappimage/legacy/test_getsection.cpp b/tests/libappimage/legacy/test_getsection.cpp
new file mode 100644 (file)
index 0000000..4546e80
--- /dev/null
@@ -0,0 +1,27 @@
+// system headers
+#include <cmath>
+
+// library headers
+#include <gtest/gtest.h>
+
+// local headers
+#include <appimage/appimage.h>
+#include "fixtures.h"
+
+extern "C" {
+    #include "getsection.h"
+}
+
+
+using namespace std;
+
+
+// most simple derivative class for better naming of the tests in this file
+class GetSectionCTest : public AppImageKitTest {};
+
+
+
+int main(int argc, char **argv) {
+    ::testing::InitGoogleTest(&argc, argv);
+    return RUN_ALL_TESTS();
+}
diff --git a/tests/libappimage/legacy/test_libappimage.cpp b/tests/libappimage/legacy/test_libappimage.cpp
new file mode 100644 (file)
index 0000000..da2b650
--- /dev/null
@@ -0,0 +1,602 @@
+#include "appimage/appimage.h"
+
+#include <cstdio>
+#include <cstring>
+#include <fstream>
+#include <memory>
+
+#include <glib.h>
+#include <glib/gstdio.h>
+
+#include <gtest/gtest.h>
+
+#include "fixtures.h"
+
+using namespace std;
+
+
+class LibAppImageTest : public TestBase {
+protected:
+    void rm_file(const std::string& path) {
+        g_remove(path.c_str());
+    }
+
+    bool areIntegrationFilesDeployed(const std::string& path) {
+        gchar* sum = appimage_get_md5(path.c_str());
+
+        GDir* dir;
+        GError* error = NULL;
+        const gchar* filename = NULL;
+
+        char* data_home = xdg_data_home();
+        char* apps_path = g_strconcat(data_home, "/applications/", NULL);
+        free(data_home);
+
+        bool found = false;
+        dir = g_dir_open(apps_path, 0, &error);
+        if (dir != NULL) {
+            while ((filename = g_dir_read_name(dir))) {
+                gchar* m = g_strrstr(filename, sum);
+
+                if (m != NULL)
+                    found = true;
+            }
+            g_dir_close(dir);
+        }
+        g_free(apps_path);
+        g_free(sum);
+        return found;
+    }
+};
+
+TEST_F(LibAppImageTest, appimage_get_type_invalid) {
+    ASSERT_EQ(appimage_get_type("/tmp", false), -1);
+}
+
+TEST_F(LibAppImageTest, appimage_get_type_on_bare_iso_9660_file) {
+    ASSERT_EQ(appimage_get_type(iso_9660_file_path.c_str(), false), -1);
+}
+
+TEST_F(LibAppImageTest, appimage_get_type_on_bare_elf_file) {
+    ASSERT_EQ(appimage_get_type(elf_file_path.c_str(), false), -1);
+}
+
+TEST_F(LibAppImageTest, appimage_get_type_1) {
+    ASSERT_EQ(appimage_get_type(appImage_type_1_file_path.c_str(), false), 1);
+}
+
+TEST_F(LibAppImageTest, appimage_get_type_on_appimage_type_1_withouth_magic_bytes) {
+    ASSERT_EQ(appimage_get_type(appImage_type_1_no_magic_file_path.c_str(), false), 1);
+}
+
+TEST_F(LibAppImageTest, appimage_get_type_2) {
+    ASSERT_EQ(appimage_get_type(appImage_type_2_file_path.c_str(), false), 2);
+}
+
+TEST_F(LibAppImageTest, appimage_unregister_in_system) {
+    ASSERT_FALSE(areIntegrationFilesDeployed(appImage_type_1_file_path));
+    ASSERT_FALSE(areIntegrationFilesDeployed(appImage_type_2_file_path));
+}
+
+TEST_F(LibAppImageTest, appimage_get_md5) {
+    std::string pathToTestFile = "/some/fixed/path";
+
+    std::string expected = "972f4824b8e6ea26a55e9af60a285af7";
+
+    gchar* sum = appimage_get_md5(pathToTestFile.c_str());
+    EXPECT_EQ(sum, expected);
+    g_free(sum);
+
+    unlink(pathToTestFile.c_str());
+}
+
+TEST_F(LibAppImageTest, get_md5_invalid_file_path) {
+    gchar* sum = appimage_get_md5("");
+
+    ASSERT_TRUE(sum == NULL) << "sum is not NULL";
+}
+
+TEST_F(LibAppImageTest, appimage_extract_file_following_symlinks) {
+    std::string target_path = tempDir + "/test_libappimage_tmp_file";
+    appimage_extract_file_following_symlinks(appImage_type_2_file_path.c_str(), "echo.desktop",
+                                             target_path.c_str());
+
+    const char expected[] = ("[Desktop Entry]\n"
+                             "Version=1.0\n"
+                             "Type=Application\n"
+                             "Name=Echo\n"
+                             "Comment=Just echo.\n"
+                             "Exec=echo %F\n"
+                             "Icon=utilities-terminal\n");
+
+    ASSERT_TRUE(g_file_test(target_path.c_str(), G_FILE_TEST_EXISTS));
+
+    std::ifstream file(target_path.c_str(), std::ios::binary | std::ios::ate);
+    std::streamsize size = file.tellg();
+    file.seekg(0, std::ios::beg);
+
+    std::vector<char> buffer(static_cast<unsigned long>(size));
+    if (file.read(buffer.data(), size))
+        ASSERT_TRUE(strncmp(expected, buffer.data(), strlen(expected)) == 0);
+    else
+        FAIL();
+
+    // Clean
+    remove(target_path.c_str());
+}
+
+TEST_F(LibAppImageTest, appimage_extract_file_following_hardlinks_type_1) {
+    const char target_file_path[] = "/tmp/appimage_tmp_file";
+    appimage_extract_file_following_symlinks(appImage_type_1_file_path.c_str(),
+                                             "AppImageExtract.png", target_file_path);
+
+
+    // using EXPECT makes sure the free call is executed
+    EXPECT_TRUE(g_file_test(target_file_path, G_FILE_TEST_EXISTS));
+    EXPECT_TRUE(g_file_test(target_file_path, G_FILE_TEST_IS_REGULAR));
+
+    struct stat stats = {};
+    lstat(target_file_path, &stats);
+    EXPECT_NE(stats.st_size, 0);
+
+    g_remove(target_file_path);
+}
+
+TEST_F(LibAppImageTest, appimage_read_file_into_buffer_following_symlinks_type_2) {
+    char* buf = NULL;
+    unsigned long bufsize = 0;
+    bool rv = appimage_read_file_into_buffer_following_symlinks(appImage_type_2_file_path.c_str(), "echo.desktop", &buf,
+                                                                &bufsize);
+
+    // using EXPECT makes sure the free call is executed
+    EXPECT_TRUE(rv);
+    EXPECT_TRUE(buf != NULL);
+    EXPECT_TRUE(bufsize != 0);
+
+    static const char expected[] = ("[Desktop Entry]\n"
+                                    "Version=1.0\n"
+                                    "Type=Application\n"
+                                    "Name=Echo\n"
+                                    "Comment=Just echo.\n"
+                                    "Exec=echo %F\n"
+                                    "Icon=utilities-terminal\n");
+
+    EXPECT_EQ(bufsize, strlen(expected));
+    EXPECT_TRUE(buf != NULL && strncmp(expected, buf, bufsize) == 0);
+    free(buf);
+}
+
+TEST_F(LibAppImageTest, appimage_read_file_into_buffer_following_symlinks_type_1) {
+    char* buf = NULL;
+    unsigned long bufsize = 0;
+    bool rv = appimage_read_file_into_buffer_following_symlinks(appImage_type_1_file_path.c_str(),
+                                                                "AppImageExtract.desktop", &buf, &bufsize);
+
+    // using EXPECT makes sure the free call is executed
+    EXPECT_TRUE(rv);
+    EXPECT_TRUE(buf != NULL);
+    EXPECT_TRUE(bufsize != 0);
+
+    static const char expected[] = ("[Desktop Entry]\n"
+                                    "Name=AppImageExtract\n"
+                                    "Exec=appimageextract\n"
+                                    "Icon=AppImageExtract\n"
+                                    "Terminal=true\n"
+                                    "Type=Application\n"
+                                    "Categories=Development;\n"
+                                    "Comment=Extract AppImage contents, part of AppImageKit\n"
+                                    "StartupNotify=true\n");
+
+    EXPECT_EQ(bufsize, strlen(expected));
+    EXPECT_TRUE(buf != NULL && strncmp(expected, buf, bufsize) == 0);
+    free(buf);
+}
+
+TEST_F(LibAppImageTest, appimage_list_files_false_appimage) {
+
+    char** files = appimage_list_files("/bin/ls");
+
+    char* expected[] = {NULL};
+
+    int i = 0;
+    for (; files[i] != NULL && expected[i] != NULL; i++)
+        EXPECT_STREQ(files[i], expected[i]);
+
+    appimage_string_list_free(files);
+
+    if (i != 0)
+        FAIL();
+}
+
+TEST_F(LibAppImageTest, appimage_list_files_type_1) {
+
+    char** files = appimage_list_files(appImage_type_1_file_path.c_str());
+
+    static const char* const expected[] = {
+        "usr",
+        "usr/bin",
+        "usr/lib",
+        "AppImageExtract.desktop",
+        ".DirIcon",
+        "AppImageExtract.png",
+        "usr/bin/appimageextract",
+        "AppRun",
+        "usr/bin/xorriso",
+        "usr/lib/libburn.so.4",
+        "usr/lib/libisoburn.so.1",
+        "usr/lib/libisofs.so.6",
+        NULL};
+
+    int i = 0;
+    for (; files[i] != NULL && expected[i] != NULL; i++)
+        EXPECT_STREQ(files[i], expected[i]);
+
+    appimage_string_list_free(files);
+    ASSERT_EQ(i, 12);
+}
+
+TEST_F(LibAppImageTest, appimage_list_files_type_2) {
+
+    char** files = appimage_list_files(appImage_type_2_file_path.c_str());
+    static const char* const expected[] = {
+        ".DirIcon",
+        "AppRun",
+        "echo.desktop",
+        "usr",
+        "usr/bin",
+        "usr/bin/echo",
+        "usr/bin",
+        "usr/share",
+        "usr/share/applications",
+        "usr/share/applications/echo.desktop",
+        "usr/share/applications",
+        "usr/share",
+        "usr",
+        "utilities-terminal.svg",
+        NULL};
+
+    int i = 0;
+    for (; files[i] != NULL && expected[i] != NULL; i++)
+        EXPECT_STREQ(files[i], expected[i]);
+
+    appimage_string_list_free(files);
+    ASSERT_EQ(i, 14);
+}
+
+TEST_F(LibAppImageTest, test_appimage_registered_desktop_file_path_not_registered) {
+    EXPECT_TRUE(appimage_registered_desktop_file_path(appImage_type_1_file_path.c_str(), NULL, false) == NULL);
+    EXPECT_TRUE(appimage_registered_desktop_file_path(appImage_type_2_file_path.c_str(), NULL, false) == NULL);
+}
+
+
+TEST_F(LibAppImageTest, test_appimage_type1_is_terminal_app) {
+    // TODO: add type 1 AppImage with Terminal=false
+    EXPECT_EQ(appimage_type1_is_terminal_app(appImage_type_1_file_path.c_str()), 1);
+    EXPECT_EQ(appimage_type1_is_terminal_app("/invalid/path"), -1);
+}
+
+TEST_F(LibAppImageTest, test_appimage_type2_is_terminal_app) {
+    EXPECT_EQ(appimage_type2_is_terminal_app(appImage_type_2_terminal_file_path.c_str()), 1);
+    EXPECT_EQ(appimage_type2_is_terminal_app(appImage_type_2_file_path.c_str()), 0);
+    EXPECT_EQ(appimage_type2_is_terminal_app("/invalid/path"), -1);
+}
+
+TEST_F(LibAppImageTest, test_appimage_is_terminal_app) {
+    EXPECT_EQ(appimage_is_terminal_app(appImage_type_1_file_path.c_str()), 1);
+    EXPECT_EQ(appimage_is_terminal_app(appImage_type_2_file_path.c_str()), 0);
+    // TODO: add type 1 AppImage with Terminal=true
+    //EXPECT_EQ(appimage_is_terminal_app(appImage_type_1_terminal_file_path.c_str()), 1);
+    EXPECT_EQ(appimage_is_terminal_app(appImage_type_2_terminal_file_path.c_str()), 1);
+    EXPECT_EQ(appimage_is_terminal_app("/invalid/path"), -1);
+}
+
+// compares whether the size first bytes of two given byte buffers are equal
+bool test_compare_bytes(const char* buf1, const char* buf2, int size) {
+    for (int i = 0; i < size; i++) {
+        if (buf1[i] != buf2[i]) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+TEST_F(LibAppImageTest, appimage_type2_digest_md5) {
+    char digest[16];
+    char expectedDigest[] = {'\xb5', '\xb9', '\x6a', '\xa3', '\x7a', '\x72', '\x7', '\x7f', '\xd8', '\xa', '\x8d', '\xae', '\xb7', '\x73', '\xed', '\x1'};
+
+    EXPECT_TRUE(appimage_type2_digest_md5(appImage_type_2_file_path.c_str(), digest));
+    EXPECT_PRED3(test_compare_bytes, digest, expectedDigest, 16);
+}
+
+#ifdef LIBAPPIMAGE_DESKTOP_INTEGRATION_ENABLED
+
+bool test_appimage_is_registered_in_system(const std::string& pathToAppImage, bool integrateAppImage) {
+    if (integrateAppImage) {
+        EXPECT_EQ(appimage_register_in_system(pathToAppImage.c_str(), false), 0);
+    }
+
+    return appimage_is_registered_in_system(pathToAppImage.c_str());
+}
+
+TEST_F(LibAppImageTest, appimage_is_registered_in_system) {
+    // make sure tested AppImages are not registered
+    appimage_unregister_in_system(appImage_type_1_file_path.c_str(), false);
+    appimage_unregister_in_system(appImage_type_2_file_path.c_str(), false);
+
+    // if the test order is false -> true, cleanup isn't necessary
+
+    // type 1 tests
+    EXPECT_FALSE(test_appimage_is_registered_in_system(appImage_type_1_file_path, false));
+    EXPECT_TRUE(test_appimage_is_registered_in_system(appImage_type_1_file_path, true));
+
+    // type 2 tests
+    EXPECT_FALSE(test_appimage_is_registered_in_system(appImage_type_2_file_path, false));
+    EXPECT_TRUE(test_appimage_is_registered_in_system(appImage_type_2_file_path, true));
+
+    // cleanup
+    appimage_unregister_in_system(appImage_type_1_file_path.c_str(), false);
+    appimage_unregister_in_system(appImage_type_2_file_path.c_str(), false);
+}
+
+TEST_F(LibAppImageTest, test_appimage_registered_desktop_file_path_type1) {
+    EXPECT_TRUE(appimage_type1_register_in_system(appImage_type_1_file_path.c_str(), false));
+
+    char* desktop_file_path = appimage_registered_desktop_file_path(appImage_type_1_file_path.c_str(), NULL, false);
+
+    EXPECT_TRUE(desktop_file_path != NULL);
+
+    free(desktop_file_path);
+}
+
+TEST_F(LibAppImageTest, test_appimage_registered_desktop_file_path_type2) {
+    EXPECT_TRUE(appimage_type2_register_in_system(appImage_type_2_file_path.c_str(), false));
+
+    char* desktop_file_path = appimage_registered_desktop_file_path(appImage_type_2_file_path.c_str(), NULL, false);
+
+    EXPECT_TRUE(desktop_file_path != NULL);
+
+    free(desktop_file_path);
+}
+
+TEST_F(LibAppImageTest, test_appimage_registered_desktop_file_path_type1_precalculated_md5) {
+    EXPECT_TRUE(appimage_type1_register_in_system(appImage_type_1_file_path.c_str(), false));
+
+    char* md5 = appimage_get_md5(appImage_type_1_file_path.c_str());
+    char* desktop_file_path = appimage_registered_desktop_file_path(appImage_type_1_file_path.c_str(), md5, false);
+    free(md5);
+
+    EXPECT_TRUE(desktop_file_path != NULL);
+
+    free(desktop_file_path);
+}
+
+TEST_F(LibAppImageTest, test_appimage_registered_desktop_file_path_type2_precalculated_md5) {
+    EXPECT_TRUE(appimage_type2_register_in_system(appImage_type_2_file_path.c_str(), false));
+
+    char* md5 = appimage_get_md5(appImage_type_2_file_path.c_str());
+    char* desktop_file_path = appimage_registered_desktop_file_path(appImage_type_2_file_path.c_str(), md5, false);
+    free(md5);
+
+    EXPECT_TRUE(desktop_file_path != NULL);
+
+    free(desktop_file_path);
+}
+
+TEST_F(LibAppImageTest, test_appimage_registered_desktop_file_path_type1_wrong_md5) {
+    EXPECT_TRUE(appimage_type1_register_in_system(appImage_type_1_file_path.c_str(), false));
+
+    char* md5 = strdup("abcdefg");
+    char* desktop_file_path = appimage_registered_desktop_file_path(appImage_type_1_file_path.c_str(), md5, false);
+    free(md5);
+
+    EXPECT_TRUE(desktop_file_path == NULL);
+
+    free(desktop_file_path);
+}
+
+TEST_F(LibAppImageTest, test_appimage_registered_desktop_file_path_type2_wrong_md5) {
+    EXPECT_TRUE(appimage_type2_register_in_system(appImage_type_2_file_path.c_str(), false));
+
+    char* md5 = strdup("abcdefg");
+    char* desktop_file_path = appimage_registered_desktop_file_path(appImage_type_2_file_path.c_str(), md5, false);
+    free(md5);
+
+    EXPECT_TRUE(desktop_file_path == NULL);
+
+    free(desktop_file_path);
+}
+
+TEST_F(LibAppImageTest, test_appimage_type2_appimage_version) {
+    EXPECT_TRUE(appimage_type2_register_in_system(appImage_type_2_versioned_path.c_str(), false));
+
+    char* desktopFilePath = appimage_registered_desktop_file_path(appImage_type_2_versioned_path.c_str(), NULL, false);
+
+    GKeyFile* desktopFile = g_key_file_new();
+
+    GError* error = NULL;
+
+    gboolean loaded = g_key_file_load_from_file(desktopFile, desktopFilePath, G_KEY_FILE_KEEP_TRANSLATIONS, &error);
+
+    if (!loaded) {
+        g_key_file_free(desktopFile);
+        ADD_FAILURE() << "Failed to read desktop file: " << error->message;
+        g_error_free(error);
+        return;
+    }
+
+    const std::string versionKey = "X-AppImage-Version";
+    const std::string oldNameKey = "X-AppImage-Old-Name";
+
+    std::string expectedVersion = "test1234";
+    gchar* actualVersion = g_key_file_get_string(desktopFile, G_KEY_FILE_DESKTOP_GROUP, versionKey.c_str(), &error);
+
+    if (actualVersion == NULL) {
+        g_key_file_free(desktopFile);
+        ADD_FAILURE() << "Failed to get " << versionKey << " key: " << error->message;
+        g_error_free(error);
+        return;
+    }
+
+    EXPECT_EQ(expectedVersion, std::string(actualVersion));
+
+    gchar* oldName = g_key_file_get_string(desktopFile, G_KEY_FILE_DESKTOP_GROUP, oldNameKey.c_str(), &error);
+
+    if (oldName == NULL) {
+        g_key_file_free(desktopFile);
+        ADD_FAILURE() << "Failed to get " << oldNameKey << " key: " << error->message;
+        g_error_free(error);
+        return;
+    }
+
+    gchar* newName = g_key_file_get_string(desktopFile, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_NAME, &error);
+
+    if (newName == NULL) {
+        g_key_file_free(desktopFile);
+        ADD_FAILURE() << "Failed to get " << G_KEY_FILE_DESKTOP_KEY_NAME << " key: " << error->message;
+        g_error_free(error);
+        return;
+    }
+
+    std::string expectedName = std::string(oldName) + " (" + expectedVersion + ")";
+
+    EXPECT_EQ(expectedName, std::string(newName));
+
+    // cleanup
+    g_key_file_free(desktopFile);
+    if (error != NULL)
+        g_error_free(error);
+}
+
+TEST_F(LibAppImageTest, appimage_register_in_system_with_type1) {
+    ASSERT_EQ(appimage_register_in_system(appImage_type_1_file_path.c_str(), false), 0);
+
+    ASSERT_TRUE(areIntegrationFilesDeployed(appImage_type_1_file_path));
+
+    appimage_unregister_in_system(appImage_type_1_file_path.c_str(), false);
+}
+
+TEST_F(LibAppImageTest, appimage_register_in_system_with_type2) {
+    ASSERT_EQ(appimage_register_in_system(appImage_type_2_file_path.c_str(), false), 0);
+
+    ASSERT_TRUE(areIntegrationFilesDeployed(appImage_type_2_file_path));
+
+    appimage_unregister_in_system(appImage_type_2_file_path.c_str(), false);
+}
+
+TEST_F(LibAppImageTest, appimage_type1_register_in_system) {
+    ASSERT_TRUE(appimage_type1_register_in_system(appImage_type_1_file_path.c_str(), false));
+
+    ASSERT_TRUE(areIntegrationFilesDeployed(appImage_type_1_file_path));
+
+    appimage_unregister_in_system(appImage_type_1_file_path.c_str(), false);
+}
+
+TEST_F(LibAppImageTest, appimage_type2_register_in_system) {
+    EXPECT_TRUE(appimage_type2_register_in_system(appImage_type_2_file_path.c_str(), false));
+
+    EXPECT_TRUE(areIntegrationFilesDeployed(appImage_type_2_file_path));
+    appimage_unregister_in_system(appImage_type_2_file_path.c_str(), false);
+}
+
+TEST_F(LibAppImageTest, test_try_exec_key_exists_type_1) {
+    const std::string& pathToAppImage = appImage_type_1_file_path;
+
+    ASSERT_EQ(appimage_register_in_system(pathToAppImage.c_str(), false), 0);
+
+    GKeyFile* kf = g_key_file_new();
+
+    const char* desktopFilePath = appimage_registered_desktop_file_path(pathToAppImage.c_str(), NULL, false);
+
+    ASSERT_TRUE(g_key_file_load_from_file(kf, desktopFilePath, G_KEY_FILE_NONE, NULL));
+
+    const char* expectedTryExecValue = g_key_file_get_string(kf, G_KEY_FILE_DESKTOP_GROUP,
+                                                             G_KEY_FILE_DESKTOP_KEY_TRY_EXEC, NULL);
+
+    EXPECT_EQ(expectedTryExecValue, pathToAppImage);
+}
+
+TEST_F(LibAppImageTest, test_try_exec_key_exists_type_2) {
+    const std::string& pathToAppImage = appImage_type_2_file_path;
+
+    ASSERT_EQ(appimage_register_in_system(pathToAppImage.c_str(), false), 0);
+
+    GKeyFile* kf = g_key_file_new();
+
+    const char* desktopFilePath = appimage_registered_desktop_file_path(pathToAppImage.c_str(), NULL, false);
+
+    ASSERT_TRUE(g_key_file_load_from_file(kf, desktopFilePath, G_KEY_FILE_NONE, NULL));
+
+    const char* expectedTryExecValue = g_key_file_get_string(kf, G_KEY_FILE_DESKTOP_GROUP,
+                                                             G_KEY_FILE_DESKTOP_KEY_TRY_EXEC, NULL);
+
+    EXPECT_EQ(expectedTryExecValue, pathToAppImage);
+}
+
+TEST_F(LibAppImageTest, test_appimage_type1_shall_not_integrate) {
+    // TODO: add type 1 AppImage with X-AppImage-Integrate=false
+    //EXPECT_EQ(appimage_is_terminal_app(appImage_type_1_shall_not_integrate_path.c_str()), 1);
+    EXPECT_EQ(appimage_type1_shall_not_be_integrated(appImage_type_1_file_path.c_str()), 0);
+//    EXPECT_EQ(appimage_type1_shall_not_be_integrated(appImage_type_2_file_path.c_str()), -1);
+    EXPECT_EQ(appimage_type1_shall_not_be_integrated("/invalid/path"), -1);
+}
+
+TEST_F(LibAppImageTest, test_appimage_type2_shall_not_integrate) {
+//    EXPECT_EQ(appimage_type2_shall_not_be_integrated(appImage_type_1_file_path.c_str()), -1);
+    EXPECT_EQ(appimage_type2_shall_not_be_integrated(appImage_type_2_shall_not_integrate_path.c_str()), 1);
+    EXPECT_EQ(appimage_type2_shall_not_be_integrated(appImage_type_2_file_path.c_str()), 0);
+    EXPECT_EQ(appimage_type2_shall_not_be_integrated("/invalid/path"), -1);
+}
+
+TEST_F(LibAppImageTest, test_appimage_shall_not_integrate) {
+    EXPECT_EQ(appimage_shall_not_be_integrated(appImage_type_1_file_path.c_str()), 0);
+    EXPECT_EQ(appimage_shall_not_be_integrated(appImage_type_2_file_path.c_str()), 0);
+    // TODO: add type 1 AppImage with X-AppImage-Integrate=false
+    //EXPECT_EQ(appimage_shall_not_be_integrated(appImage_type_1_shall_not_integrate_path.c_str()), 1);
+    EXPECT_EQ(appimage_shall_not_be_integrated(appImage_type_2_shall_not_integrate_path.c_str()), 1);
+    EXPECT_EQ(appimage_shall_not_be_integrated("/invalid/path"), -1);
+}
+
+#ifdef LIBAPPIMAGE_THUMBNAILER_ENABLED
+
+TEST_F(LibAppImageTest, create_thumbnail_appimage_type_1) {
+    EXPECT_TRUE(appimage_create_thumbnail(appImage_type_1_file_path.c_str(), false));
+
+    gchar* sum = appimage_get_md5(appImage_type_1_file_path.c_str());
+
+    char* cache_home = xdg_cache_home();
+    std::string path = std::string(cache_home)
+                       + "/thumbnails/normal/"
+                       + std::string(sum) + ".png";
+
+    g_free(cache_home);
+    g_free(sum);
+
+    ASSERT_TRUE(g_file_test(path.c_str(), G_FILE_TEST_EXISTS));
+
+    // Clean
+    rm_file(path);
+}
+
+TEST_F(LibAppImageTest, create_thumbnail_appimage_type_2) {
+    EXPECT_TRUE(appimage_create_thumbnail(appImage_type_2_file_path.c_str(), false));
+
+    gchar* sum = appimage_get_md5(appImage_type_2_file_path.c_str());
+
+    char* cache_home = xdg_cache_home();
+    std::string path = std::string(cache_home)
+                       + "/thumbnails/normal/"
+                       + std::string(sum) + ".png";
+
+    g_free(cache_home);
+    g_free(sum);
+
+    ASSERT_TRUE(g_file_test(path.c_str(), G_FILE_TEST_EXISTS));
+
+    // Clean
+    rm_file(path);
+}
+
+#endif // LIBAPPIMAGE_THUMBNAILER_ENABLED
+
+#endif // LIBAPPIMAGE_DESKTOP_INTEGRATION_ENABLED
diff --git a/tests/libappimage/legacy/test_shared.cpp b/tests/libappimage/legacy/test_shared.cpp
new file mode 100644 (file)
index 0000000..8595fa8
--- /dev/null
@@ -0,0 +1,94 @@
+#include <gtest/gtest.h>
+#include <fstream>
+#include <ftw.h>
+#include <string>
+#include <sstream>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "fixtures.h"
+
+extern "C" {
+    #include <appimage/appimage_shared.h>
+}
+
+
+using namespace std;
+
+
+// most simple derivative class for better naming of the tests in this file
+class LibAppImageSharedTest : public TestBase {};
+
+
+static bool test_strcmp(const char* a, const char* b) {
+    return strcmp(a, b) == 0;
+}
+
+TEST_F(LibAppImageSharedTest, test_appimage_hexlify) {
+    {
+        char bytesIn[] = "\x00\x01\x02\x03\x04\x05\x06\x07";
+        char expectedHex[] = "0001020304050607";
+
+        char* hexlified = appimage_hexlify(bytesIn, 8);
+        EXPECT_PRED2(test_strcmp, hexlified, expectedHex);
+
+        // cleanup
+        free(hexlified);
+    }
+    {
+        char bytesIn[] = "\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff";
+        char expectedHex[] = "f8f9fafbfcfdfeff";
+
+        char* hexlified = appimage_hexlify(bytesIn, 8);
+        EXPECT_PRED2(test_strcmp, hexlified, expectedHex);
+
+        // cleanup
+        free(hexlified);
+    }
+}
+
+
+bool isPowerOfTwo(int number) {
+    return (number & (number - 1)) == 0;
+}
+
+
+TEST_F(LibAppImageSharedTest, test_appimage_get_elf_section_offset_and_length) {
+    std::string appImagePath = std::string(TEST_DATA_DIR) + "/appimaged-i686.AppImage";
+
+    unsigned long offset, length;
+
+    ASSERT_TRUE(appimage_get_elf_section_offset_and_length(appImagePath.c_str(), ".upd_info", &offset, &length));
+
+    EXPECT_GT(offset, 0);
+    EXPECT_GT(length, 0);
+
+    EXPECT_PRED1(isPowerOfTwo, length);
+}
+
+
+TEST_F(LibAppImageSharedTest, test_print_binary) {
+    std::string appImagePath = std::string(TEST_DATA_DIR) + "/appimaged-i686.AppImage";
+
+    unsigned long offset, length;
+
+    ASSERT_TRUE(appimage_get_elf_section_offset_and_length(appImagePath.c_str(), ".upd_info", &offset, &length));
+
+    EXPECT_EQ(appimage_print_binary(appImagePath.c_str(), offset, length), 0);
+}
+
+
+TEST_F(LibAppImageSharedTest, test_print_hex) {
+    std::string appImagePath = std::string(TEST_DATA_DIR) + "/appimaged-i686.AppImage";
+
+    unsigned long offset, length;
+
+    ASSERT_TRUE(appimage_get_elf_section_offset_and_length(appImagePath.c_str(), ".sha256_sig", &offset, &length));
+
+    EXPECT_EQ(appimage_print_hex(appImagePath.c_str(), offset, length), 0);
+}
+
+int main(int argc, char **argv) {
+    ::testing::InitGoogleTest(&argc, argv);
+    return RUN_ALL_TESTS();
+}
diff --git a/tests/libappimage/utils/StringSanitizerTest.cpp b/tests/libappimage/utils/StringSanitizerTest.cpp
new file mode 100644 (file)
index 0000000..d2f8e65
--- /dev/null
@@ -0,0 +1,31 @@
+// library includes
+#include <gtest/gtest.h>
+
+// local includes
+#include "utils/StringSanitizer.h"
+
+class StringSanitizerTest : public ::testing::Test {};
+
+TEST_F(StringSanitizerTest, testSanitizeForPathWithEmptyPath) {
+    const auto actual = StringSanitizer("").sanitizeForPath();
+    EXPECT_TRUE(actual.empty());
+}
+
+TEST_F(StringSanitizerTest, testSanitizeForPathWithAlreadySafeString) {
+    const std::string alreadySafeString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.";
+    EXPECT_TRUE(alreadySafeString == StringSanitizer(alreadySafeString).sanitizeForPath());
+}
+
+TEST_F(StringSanitizerTest, testSanitizeForPathWithUnsafePath) {
+    const std::string unsafeString = "/../$ยง%&testabcdefg";
+    const std::string expected = "_..______testabcdefg";
+    const auto actual = StringSanitizer(unsafeString).sanitizeForPath();
+    EXPECT_TRUE(expected == actual);
+}
+
+TEST_F(StringSanitizerTest, testSanitizeForPathWithSpaces) {
+    const std::string unsafeString = "test string abcdefg hijklmnop ";
+    const std::string expected = "test_string_abcdefg_hijklmnop_";
+    const auto actual = StringSanitizer(unsafeString).sanitizeForPath();
+    EXPECT_TRUE(expected == actual);
+}
diff --git a/tests/libappimage/utils/TestIconHandle.cpp b/tests/libappimage/utils/TestIconHandle.cpp
new file mode 100644 (file)
index 0000000..228214b
--- /dev/null
@@ -0,0 +1,92 @@
+// system
+#include <fstream>
+
+// libraries
+#include <gtest/gtest.h>
+#include <boost/filesystem.hpp>
+
+// local
+#include "utils/IconHandle.h"
+
+using namespace appimage::utils;
+namespace bf = boost::filesystem;
+
+
+TEST(TestUtilsIconHandle, loadFilePng) {
+    IconHandle handle(TEST_DATA_DIR "squashfs-root/utilities-terminal.png");
+
+    ASSERT_EQ(handle.format(), "png");
+    ASSERT_EQ(handle.getSize(), 48);
+}
+
+TEST(TestUtilsIconHandle, savePngUnchanged) {
+    IconHandle handle(TEST_DATA_DIR "squashfs-root/utilities-terminal.png");
+
+    ASSERT_EQ(handle.format(), "png");
+    ASSERT_EQ(handle.getSize(), 48);
+
+    auto tmpPath = bf::temp_directory_path() / bf::unique_path();
+
+    ASSERT_NO_THROW(handle.save(tmpPath.string(), "png"));
+    ASSERT_TRUE(bf::exists(tmpPath));
+    ASSERT_FALSE(bf::is_empty(tmpPath));
+
+    bf::remove(tmpPath);
+}
+
+TEST(TestUtilsIconHandle, savePngResized) {
+    IconHandle handle(TEST_DATA_DIR "squashfs-root/utilities-terminal.png");
+
+    ASSERT_EQ(handle.format(), "png");
+    ASSERT_EQ(handle.getSize(), 48);
+
+    handle.setSize(256);
+
+    auto tmpPath = bf::temp_directory_path() / bf::unique_path();
+
+    ASSERT_NO_THROW(handle.save(tmpPath.string(), "png"));
+
+    IconHandle handle2(tmpPath.string());
+    ASSERT_EQ(handle2.format(), "png");
+    ASSERT_EQ(handle2.getSize(), 256);
+
+    bf::remove(tmpPath);
+}
+
+TEST(TestUtilsIconHandle, loadFileSvg) {
+    IconHandle handle(TEST_DATA_DIR "squashfs-root/utilities-terminal.svg");
+
+    ASSERT_EQ(handle.format(), "svg");
+    ASSERT_EQ(handle.getSize(), 48);
+}
+
+
+TEST(TestUtilsIconHandle, saveSvg) {
+    IconHandle handle(TEST_DATA_DIR "squashfs-root/utilities-terminal.svg");
+
+    auto tmpPath = bf::temp_directory_path() / bf::unique_path();
+
+
+    ASSERT_NO_THROW(handle.save(tmpPath.string(), "svg"));
+
+    IconHandle handle2(tmpPath.string());
+    ASSERT_EQ(handle2.format(), "svg");
+    ASSERT_EQ(handle2.getSize(), 48);
+
+    bf::remove(tmpPath);
+}
+
+TEST(TestUtilsIconHandle, saveSvgAsPng) {
+    IconHandle handle(TEST_DATA_DIR "squashfs-root/utilities-terminal.svg");
+
+    auto tmpPath = bf::temp_directory_path() / bf::unique_path();
+
+    handle.setSize(256);
+    ASSERT_NO_THROW(handle.save(tmpPath.string(), "png"));
+
+    IconHandle handle2(tmpPath.string());
+    ASSERT_EQ(handle2.format(), "png");
+    ASSERT_EQ(handle2.getSize(), 256);
+
+    bf::remove(tmpPath);
+}
diff --git a/tests/libappimage/utils/TestLogger.cpp b/tests/libappimage/utils/TestLogger.cpp
new file mode 100644 (file)
index 0000000..51c788a
--- /dev/null
@@ -0,0 +1,30 @@
+// system
+#include <ostream>
+
+// libraries
+#include <gtest/gtest.h>
+
+// local
+#include "utils/Logger.h"
+
+using namespace appimage::utils;
+
+TEST(TestLogger, instance) {
+    ASSERT_TRUE(Logger::getInstance() != NULL);
+}
+
+TEST(TestLogger, setCallback) {
+    auto logger = Logger::getInstance();
+
+    LogLevel levelSet;
+    std::string messageSet;
+    logger->setCallback([&levelSet, &messageSet](const LogLevel& level, const std::string& message) {
+        levelSet = level;
+        messageSet = message;
+    });
+
+    logger->log(LogLevel::ERROR, "Hello");
+
+    ASSERT_EQ(levelSet, LogLevel::ERROR);
+    ASSERT_EQ(messageSet, "Hello");
+}
diff --git a/tests/libappimage/utils/TestMagicBytesChecker.cpp b/tests/libappimage/utils/TestMagicBytesChecker.cpp
new file mode 100644 (file)
index 0000000..6d9bf6e
--- /dev/null
@@ -0,0 +1,33 @@
+// libraries
+#include <gtest/gtest.h>
+
+// local
+#include "utils/MagicBytesChecker.h"
+
+using namespace appimage::utils;
+
+TEST(MagicBytesCheckerTests, hasIso9660Signature) {
+    ASSERT_TRUE(MagicBytesChecker(TEST_DATA_DIR "/minimal.iso").hasIso9660Signature());
+    ASSERT_TRUE(MagicBytesChecker(TEST_DATA_DIR "/AppImageExtract_6-x86_64.AppImage").hasIso9660Signature());
+    ASSERT_FALSE(MagicBytesChecker(TEST_DATA_DIR "/Cura.desktop").hasIso9660Signature());
+}
+
+TEST(MagicBytesCheckerTests, hasElfSignature) {
+    ASSERT_TRUE(MagicBytesChecker(TEST_DATA_DIR "/elffile").hasElfSignature());
+    ASSERT_TRUE(MagicBytesChecker(TEST_DATA_DIR "/appimagetool-x86_64.AppImage").hasElfSignature());
+    ASSERT_FALSE(MagicBytesChecker(TEST_DATA_DIR "/Cura.desktop").hasElfSignature());
+}
+
+TEST(MagicBytesCheckerTests, hasAppImageType1Signature) {
+    ASSERT_TRUE(MagicBytesChecker(TEST_DATA_DIR "/AppImageExtract_6-x86_64.AppImage").hasAppImageType1Signature());
+    ASSERT_FALSE(MagicBytesChecker(TEST_DATA_DIR "/appimagetool-x86_64.AppImage").hasAppImageType1Signature());
+    ASSERT_FALSE(MagicBytesChecker(TEST_DATA_DIR "/elffile").hasAppImageType1Signature());
+    ASSERT_FALSE(MagicBytesChecker(TEST_DATA_DIR "/Cura.desktop").hasAppImageType1Signature());
+}
+
+TEST(MagicBytesCheckerTests, hasAppImageType2Signature) {
+    ASSERT_TRUE(MagicBytesChecker(TEST_DATA_DIR "/appimagetool-x86_64.AppImage").hasAppImageType2Signature());
+    ASSERT_FALSE(MagicBytesChecker(TEST_DATA_DIR "/AppImageExtract_6-x86_64.AppImage").hasAppImageType2Signature());
+    ASSERT_FALSE(MagicBytesChecker(TEST_DATA_DIR "/elffile").hasAppImageType2Signature());
+    ASSERT_FALSE(MagicBytesChecker(TEST_DATA_DIR "/Cura.desktop").hasAppImageType2Signature());
+}
diff --git a/tests/libappimage/utils/TestPayloadEntriesCache.cpp b/tests/libappimage/utils/TestPayloadEntriesCache.cpp
new file mode 100644 (file)
index 0000000..ed821c5
--- /dev/null
@@ -0,0 +1,62 @@
+// system
+#include <sstream>
+
+// library headers
+#include <gtest/gtest.h>
+#include <boost/filesystem.hpp>
+
+// local
+#include "utils/resources_extractor/PayloadEntriesCache.h"
+
+using namespace appimage::utils;
+
+class TestPayloadEntriesCache : public ::testing::Test {
+protected:
+    std::shared_ptr<appimage::core::AppImage> appImage;
+    std::shared_ptr<PayloadEntriesCache> entriesCache;
+
+    void SetUp() override {
+        appImage = std::make_shared<appimage::core::AppImage>(TEST_DATA_DIR "appimagetool-x86_64.AppImage");
+        entriesCache = std::make_shared<PayloadEntriesCache>(*appImage);
+    }
+};
+
+TEST_F(TestPayloadEntriesCache, getEntriesPath) {
+    auto paths = entriesCache->getEntriesPaths();
+    std::vector<std::string> expectedPaths = {
+        ".DirIcon", "AppRun", "appimagetool.desktop", "appimagetool.svg", "usr", "usr/bin", "usr/bin/appimagetool",
+        "usr/bin/desktop-file-validate", "usr/bin/file", "usr/bin/zsyncmake", "usr/lib", "usr/lib/appimagekit",
+        "usr/lib/appimagekit/mksquashfs", "usr/share", "usr/share/metainfo",
+        "usr/share/metainfo/appimagetool.appdata.xml",
+    };
+
+    ASSERT_EQ(paths, expectedPaths);
+}
+
+TEST_F(TestPayloadEntriesCache, getEntryTypeRegular) {
+    auto type = entriesCache->getEntryType("appimagetool.svg");
+    ASSERT_EQ(type, appimage::core::PayloadEntryType::REGULAR);
+}
+
+TEST_F(TestPayloadEntriesCache, getEntryTypeLink) {
+    auto type = entriesCache->getEntryType(".DirIcon");
+    ASSERT_EQ(type, appimage::core::PayloadEntryType::LINK);
+}
+
+TEST_F(TestPayloadEntriesCache, getEntryTypeDir) {
+    auto type = entriesCache->getEntryType("usr");
+    ASSERT_EQ(type, appimage::core::PayloadEntryType::DIR);
+}
+
+TEST_F(TestPayloadEntriesCache, getLinkTarget1) {
+    auto target = entriesCache->getEntryLinkTarget(".DirIcon");
+    ASSERT_EQ(target, "appimagetool.svg");
+}
+
+TEST_F(TestPayloadEntriesCache, getLinkTargetNotLink) {
+    ASSERT_THROW(entriesCache->getEntryLinkTarget("echo.destkop"), appimage::core::PayloadIteratorError);
+}
+
+TEST_F(TestPayloadEntriesCache, getMissingLinkTarget) {
+    ASSERT_THROW(entriesCache->getEntryLinkTarget("missing"), appimage::core::PayloadIteratorError);
+}
diff --git a/tests/libappimage/utils/TestResourcesExtractor.cpp b/tests/libappimage/utils/TestResourcesExtractor.cpp
new file mode 100644 (file)
index 0000000..c869ba7
--- /dev/null
@@ -0,0 +1,72 @@
+// system
+#include <sstream>
+
+// library headers
+#include <gtest/gtest.h>
+#include <boost/filesystem.hpp>
+#include <XdgUtils/DesktopEntry/DesktopEntry.h>
+
+// local
+#include <appimage/utils/ResourcesExtractor.h>
+
+using namespace appimage::utils;
+using namespace XdgUtils::DesktopEntry;
+namespace bf = boost::filesystem;
+
+TEST(TestResourcesExtractor, getDesktopEntryPath) {
+    appimage::core::AppImage appImage(TEST_DATA_DIR "appimagetool-x86_64.AppImage");
+    ResourcesExtractor extractor(appImage);
+
+    auto desktopEntryPath = extractor.getDesktopEntryPath();
+
+    ASSERT_EQ(desktopEntryPath, "appimagetool.desktop");
+}
+
+TEST(TestResourcesExtractor, getIconPaths) {
+    /* We need to edit the echo AppImage to properly tests this feature */
+
+//    appimage::core::AppImage appImage(TEST_DATA_DIR "Echo-x86_64.AppImage");
+//    ResourcesExtractor extractor(appImage);
+//
+//    auto iconFilePaths = extractor.getIconFilePaths("utilities-terminal");
+//
+//    const std::vector<std::string> expected = {"usr/share/icons/hicolor/scalable/utilities-terminal.svg"};
+//    ASSERT_EQ(iconFilePaths,  expected);
+}
+
+TEST(TestResourcesExtractor, extractEntriesTo) {
+    appimage::core::AppImage appImage(TEST_DATA_DIR "Echo-x86_64.AppImage");
+    ResourcesExtractor extractor(appImage);
+
+    auto tempFile = bf::temp_directory_path() / boost::filesystem::unique_path("libappimage-%%%%-%%%%-%%%%-%%%%");;
+    std::map<std::string, std::string> map = {{".DirIcon", tempFile.string()}};
+    extractor.extractTo(map);
+
+    ASSERT_TRUE(bf::exists(tempFile));
+    ASSERT_TRUE(bf::file_size(tempFile) > 0);
+
+    bf::remove(tempFile);
+}
+
+TEST(TestResourcesExtractor, extractOne) {
+    appimage::core::AppImage appImage(TEST_DATA_DIR "Echo-x86_64.AppImage");
+    ResourcesExtractor extractor(appImage);
+
+    auto fileData = extractor.extract("echo.desktop");
+
+    ASSERT_FALSE(fileData.empty());
+    ASSERT_THROW(extractor.extract("missing_file"), appimage::core::PayloadIteratorError);
+}
+
+TEST(TestResourcesExtractor, extractMany) {
+    appimage::core::AppImage appImage(TEST_DATA_DIR "Echo-x86_64.AppImage");
+    ResourcesExtractor extractor(appImage);
+
+    auto filesData = extractor.extract(std::vector<std::string>{"echo.desktop", ".DirIcon"});
+
+    ASSERT_FALSE(filesData.empty());
+    for (const auto& itr: filesData)
+        ASSERT_FALSE(itr.second.empty());
+
+    ASSERT_THROW(extractor.extract(std::vector<std::string>{"missing_file"}), appimage::core::PayloadIteratorError);
+}
diff --git a/tests/libappimage/utils/TestUtilsElf.cpp b/tests/libappimage/utils/TestUtilsElf.cpp
new file mode 100644 (file)
index 0000000..0e46b5e
--- /dev/null
@@ -0,0 +1,14 @@
+// libraries
+#include <gtest/gtest.h>
+
+// local
+#include "utils/ElfFile.h"
+
+using namespace appimage::utils;
+
+TEST(TestUtilsElf, getSize) {
+    ASSERT_EQ(ElfFile(TEST_DATA_DIR "/AppImageExtract_6-x86_64.AppImage").getSize(), 28040);
+    ASSERT_EQ(ElfFile(TEST_DATA_DIR "/AppImageExtract_6_no_magic_bytes-x86_64.AppImage").getSize(), 28040);
+    ASSERT_EQ(ElfFile(TEST_DATA_DIR "/Echo-x86_64.AppImage").getSize(), 187784);
+    ASSERT_EQ(ElfFile(TEST_DATA_DIR "/appimaged-i686.AppImage").getSize(), 91148);
+}