File src.obscpio of Package AppImageLauncher

07070100000000000081a400000000000000000000000168cf6940000006e3000000000000000000000000000000000000001300000000src/CMakeLists.txt# in version 3.6 IMPORTED_TARGET has been added to pkg_*_modules
cmake_minimum_required(VERSION 3.6)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)

# Find includes in corresponding build directories
set(CMAKE_INCLUDE_CURRENT_DIR ON)
# Instruct CMake to run moc automatically when needed
set(CMAKE_AUTOMOC ON)
# Create code from a list of Qt designer ui files
set(CMAKE_AUTOUIC ON)
# Compile resource files
set(CMAKE_AUTORCC ON)

find_package(Qt5 REQUIRED COMPONENTS Core Widgets DBus Quick QuickWidgets Qml)

find_package(PkgConfig REQUIRED)
pkg_check_modules(glib REQUIRED glib-2.0>=2.40 IMPORTED_TARGET)

find_package(INotify REQUIRED)

# disable Qt debug messages except for debug builds
if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
    message(STATUS "CMake build type is ${CMAKE_BUILD_TYPE}, hence disabling debug messages in AppImageLauncher")
    add_definitions(-DQT_NO_DEBUG_OUTPUT)
    add_definitions(-DQT_NO_INFO_OUTPUT)
    add_definitions(-DQT_NO_WARNING_OUTPUT)
endif()

# this shall propagate to all targets built in the subdirs
if(ENABLE_UPDATE_HELPER)
    add_definitions(-DENABLE_UPDATE_HELPER)
endif()

add_compile_options(-Wall -Wpedantic)

if(BUILD_LITE)
    add_definitions(-DBUILD_LITE)
endif()

# utility libraries
add_subdirectory(fswatcher)
add_subdirectory(i18n)
add_subdirectory(trashbin)
add_subdirectory(shared)

# appimagelauncherd
add_subdirectory(daemon)

# main application and helper tools
add_subdirectory(ui)

# no need to include bypass launcher in lite builds
if(NOT BUILD_LITE)
    add_subdirectory(binfmt-bypass)
endif()

# CLI helper allowing other tools to utilize AppImageLauncher's code for e.g., integrating AppImages
add_subdirectory(cli)
07070100000001000081a400000000000000000000000168cf694000001ed9000000000000000000000000000000000000002100000000src/binfmt-bypass/CMakeLists.txt# needed for LINK_OPTIONS/target_link_options
cmake_minimum_required(VERSION 3.13)

project(appimage-binfmt-bypass)

# configure names globally
set(bypass_bin binfmt-bypass)
set(binfmt_interpreter binfmt-interpreter)
set(bypass_lib lib${bypass_bin})
set(preload_lib ${bypass_bin}-preload)
# we're not using "i386" or "armhf" but instead the more generic "32bit" suffix, making things a little easier in the
# code
set(preload_lib_32bit ${preload_lib}_32bit)

include_directories(${CMAKE_CURRENT_SOURCE_DIR})

string(TOLOWER ${CMAKE_C_COMPILER_ID} lower_cmake_c_compiler_id)

# on 64-bit systems such as x86_64 or aarch64/arm64v8, we need to provide a 32-bit preload library to allow the users
# to execute compatible 32-bit binaries, as the regular preload library cannot be preloaded due to the architecture
# being incompatible
# unfortunately, compiling on ARM is significantly harder than just adding a compiler flag, as gcc-multilib is missing
# it's likely easier with clang, but that remains to be verified
if(CMAKE_SYSTEM_PROCESSOR MATCHES x86_64)
    message(STATUS "64-bit target detected (${CMAKE_SYSTEM_PROCESSOR}), building 32-bit preload library as well")
    set(build_32bit_preload_library true)
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES arm64 OR CMAKE_SYSTEM_PROCESSOR MATCHES aarch64)
    string(TOLOWER ${CMAKE_C_COMPILER_ID} lower_cmake_c_compiler_id)

    if(lower_cmake_c_compiler_id MATCHES clang)
        # clang-3.8, the default clang on xenial, doesn't like the templates in elf.cpp with the varying return types
        if(NOT ${CMAKE_CXX_COMPILER_VERSION} VERSION_GREATER 3.9.0)
            message(FATAL_ERROR "clang too old, please upgrade to, e.g., clang-8")
        endif()

        message(STATUS "64-bit target detected (${CMAKE_SYSTEM_PROCESSOR}), building 32-bit preload library as well")
        set(build_32bit_preload_library true)
    else()
        message(WARNING "64-bit target detected (${CMAKE_SYSTEM_PROCESSOR}), but compiling with ${lower_cmake_c_compiler_id}, cannot build 32-bit preload library")
    endif()
endif()

function(make_preload_lib_target target_name)
    # library to be preloaded when launching the patched runtime binary
    # we need to build with -fPIC, otherwise we can't use it with $LD_PRELOAD
    add_library(${target_name} SHARED preload.c logging.h)
    target_link_libraries(${target_name} PRIVATE dl)
    target_compile_options(${target_name}
        PRIVATE -fPIC
        PRIVATE -DCOMPONENT_NAME="preload"
        # hide all symbols by default
        PRIVATE -fvisibility=hidden
    )
    # compatibility with CMake < 3.13
    set_target_properties(${target_name} PROPERTIES LINK_OPTIONS
        # hide all symbols by default
        -fvisibility=hidden
    )

    # a bit of a hack, but it seems to make binfmt bypass work in really old Docker images (e.g., CentOS <= 7)
    add_custom_command(
        TARGET ${target_name}
        POST_BUILD
        COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/fix-preload-library.sh $<TARGET_FILE_NAME:${target_name}>
        WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
        VERBATIM
    )
endfunction()

make_preload_lib_target(${preload_lib})

if(build_32bit_preload_library)
    make_preload_lib_target(${preload_lib_32bit})

    if(CMAKE_SYSTEM_PROCESSOR MATCHES x86_64)
        if(lower_cmake_c_compiler_id MATCHES clang)
            target_compile_options(${preload_lib_32bit} PRIVATE "--target=i386-linux-gnu")
            target_link_options(${preload_lib_32bit} PRIVATE "--target=i386-linux-gnu")
        else()
            # GCC style flags
            target_compile_options(${preload_lib_32bit} PRIVATE "-m32")
            target_link_options(${preload_lib_32bit} PRIVATE "-m32")
        endif()
    elseif(CMAKE_SYSTEM_PROCESSOR MATCHES arm64 OR CMAKE_SYSTEM_PROCESSOR MATCHES aarch64)
        target_compile_options(${preload_lib_32bit} PRIVATE "--target=arm-linux-gnueabihf")
        target_link_options(${preload_lib_32bit} PRIVATE "--target=arm-linux-gnueabihf")
    else()
        message(FATAL_ERROR "unknown processor architecture: ${CMAKE_SYSTEM_PROCESSOR}")
    endif()
endif()

# we need to make the library below fully self-contained so that it works in other cgroups (e.g., in Docker containers)
# out of the box
# this allows us to check whether the path is available, and otherwise create a temporary file and use that then
# this is a workaround to existing issues using AppImages in Docker with AppImageLauncher installed on the host system
check_program(NAME xxd)

function(generate_preload_lib_header target_name)
    add_custom_command(
        OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${target_name}.h
        COMMAND xxd -i $<TARGET_FILE_NAME:${target_name}> ${target_name}.h
        WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
        DEPENDS ${target_name}
        VERBATIM
    )
endfunction()

generate_preload_lib_header(${preload_lib})

# same story for 32-bit lib
if (build_32bit_preload_library)
    generate_preload_lib_header(${preload_lib_32bit})
endif()

# the lib provides an algorithm to extract the runtime, patch it and launch it, preloading our preload lib to make the
# AppImage think it is launched normally
# static linking is preferred, since we do not want to deal with an installed .so file, rpaths etc.
add_library(${bypass_lib} STATIC lib.cpp elf.cpp logging.h elf.h ${CMAKE_CURRENT_BINARY_DIR}/${preload_lib}.h)
target_link_libraries(${bypass_lib} PUBLIC dl)
# we need to include the preload lib headers (see below) from the binary dir
target_include_directories(${bypass_lib} PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_compile_options(${bypass_lib}
    PRIVATE -DPRELOAD_LIB_NAME="$<TARGET_FILE_NAME:${preload_lib}>"
    PUBLIC -D_GNU_SOURCE
    # obviously needs to be private, otherwise the value leaks into users of this lib
    PRIVATE -DCOMPONENT_NAME="lib"
)
if(build_32bit_preload_library)
    target_compile_options(${bypass_lib}
        PRIVATE -DPRELOAD_LIB_NAME_32BIT="$<TARGET_FILE_NAME:${preload_lib_32bit}>"
    )
    target_sources(${bypass_lib} PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/${preload_lib_32bit}.h)
endif()

include(CheckCSourceCompiles)
message(STATUS "Checking whether memfd_create(...) is available")
check_c_source_compiles("
    #define _GNU_SOURCE
    #include <sys/mman.h>
    int main(int argc, char** argv) {
        memfd_create(\"test\", 0);
    }
    "
    HAVE_MEMFD_CREATE
)

if(HAVE_MEMFD_CREATE)
    target_compile_options(${bypass_lib} PUBLIC -DHAVE_MEMFD_CREATE)
else()
    message(WARNING "memfd_create not available, falling back to shm_open")
    target_link_libraries(${bypass_lib} PRIVATE rt)
endif()

add_executable(${bypass_bin} bypass_main.cpp)
target_link_libraries(${bypass_bin} ${bypass_lib})
target_compile_options(${bypass_bin}
    PRIVATE -DCOMPONENT_NAME="bin"
)

add_executable(${binfmt_interpreter} interpreter_main.cpp)
target_link_libraries(${binfmt_interpreter} ${bypass_lib})
target_compile_options(${binfmt_interpreter}
    PRIVATE -DCOMPONENT_NAME="interpreter"
    PRIVATE -DAPPIMAGELAUNCHER_PATH="${CMAKE_INSTALL_PREFIX}/${_bindir}/$<TARGET_FILE_NAME:AppImageLauncher>"
)
# static linking makes the F (fix binary) mode more reliable, as the binary as well as all its resources are completely
# preloaded by binfmt-misc, avoiding runtime dependencies on libc, libstdc++ etc.
target_link_options(${binfmt_interpreter}
    PRIVATE -static -static-libgcc -static-libstdc++
)

# no need to set rpath on bypass binary, it doesn't depend on any private libraries
# the preload lib is used via its absolute path anyway

install(
    TARGETS ${bypass_bin} ${preload_lib} ${binfmt_interpreter}
    RUNTIME DESTINATION ${_private_libdir} COMPONENT APPIMAGELAUNCHER
    LIBRARY DESTINATION ${_private_libdir} COMPONENT APPIMAGELAUNCHER
)
if(build_32bit_preload_library)
    install(
        TARGETS ${preload_lib}
        LIBRARY DESTINATION ${_private_libdir}
    )
endif()
07070100000002000081a400000000000000000000000168cf69400000016e000000000000000000000000000000000000002200000000src/binfmt-bypass/bypass_main.cpp// system headers
#include <vector>

// own headers
#include "logging.h"
#include "lib.h"

int main(int argc, char** argv) {
    if (argc <= 1) {
        log_message("Usage: %s <AppImage file> [args...]\n", argv[0]);
        return EXIT_CODE_FAILURE;
    }

    std::vector<char*> args(&argv[2], &argv[argc]);
    return bypassBinfmtAndRunAppImage(argv[1], args);
}
07070100000003000081a400000000000000000000000168cf6940000018c1000000000000000000000000000000000000001a00000000src/binfmt-bypass/elf.cpp// system headers
#include <cstdio>
#include <cstdlib>
#include <cstdint>
#include <type_traits>
#include <linux/elf.h>
#include <byteswap.h>
#include <fstream>
#include <iostream>
#include <stdexcept>

// own headers
#include "elf.h"
#include "logging.h"

#if __BYTE_ORDER == __LITTLE_ENDIAN
#define NATIVE_BYTE_ORDER ELFDATA2LSB
#elif __BYTE_ORDER == __BIG_ENDIAN
#define NATIVE_BYTE_ORDER ELFDATA2MSB
#else
#error "Unknown machine endian"
#endif

template<typename T>
T bswap(T val) = delete;

template<>
uint16_t bswap(uint16_t val) {
    return bswap_16(val);
}

template<>
uint32_t bswap(uint32_t val) {
    return bswap_32(val);
}

template<>
unsigned long long bswap(unsigned long long val) {
    return bswap_64(val);
}

template<typename EhdrT, typename ValT>
void swap_data_if_necessary(const EhdrT& ehdr, ValT& val) {
    static_assert(std::is_same<Elf64_Ehdr, EhdrT>::value || std::is_same<Elf32_Ehdr, EhdrT>::value,
                  "must be Elf{32,64}_Ehdr");

    if (ehdr.e_ident[EI_DATA] != NATIVE_BYTE_ORDER) {
        val = bswap(val);
    }
}

template<typename EhdrT, typename ShdrT>
off_t get_elf_size(std::ifstream& ifs)
{
    static_assert(std::is_same<Elf64_Ehdr, EhdrT>::value || std::is_same<Elf32_Ehdr, EhdrT>::value,
                  "must be Elf{32,64}_Ehdr");
    static_assert(std::is_same<Elf64_Shdr, ShdrT>::value || std::is_same<Elf32_Shdr, ShdrT>::value,
                  "must be Elf{32,64}_Shdr");

    EhdrT elf_header{};

    ifs.seekg(0, std::ifstream::beg);
    ifs.read(reinterpret_cast<char*>(&elf_header), sizeof(elf_header));

    if (!ifs) {
        log_error("failed to read ELF header\n");
        return -1;
    }

    swap_data_if_necessary(elf_header, elf_header.e_shoff);
    swap_data_if_necessary(elf_header, elf_header.e_shentsize);
    swap_data_if_necessary(elf_header, elf_header.e_shnum);
    swap_data_if_necessary(elf_header, elf_header.e_shnum);

    off_t last_shdr_offset = elf_header.e_shoff + (elf_header.e_shentsize * (elf_header.e_shnum - 1));
    ShdrT section_header{};

    ifs.seekg(last_shdr_offset, std::ifstream::beg);
    ifs.read(reinterpret_cast<char*>(&section_header), sizeof(elf_header));

    if (!ifs) {
        log_error("failed to read ELF section header\n");
        return -1;
    }

    swap_data_if_necessary(elf_header, section_header.sh_offset);
    swap_data_if_necessary(elf_header, section_header.sh_size);

    /* ELF ends either with the table of section headers (SHT) or with a section. */
    off_t sht_end = elf_header.e_shoff + (elf_header.e_shentsize * elf_header.e_shnum);
    off_t last_section_end = section_header.sh_offset + section_header.sh_size;
    return sht_end > last_section_end ? sht_end : last_section_end;
}

template<typename EhdrT, typename ShdrT>
ssize_t get_pt_dynamic_offset(std::ifstream& ifs)
{
    static_assert(std::is_same<Elf64_Ehdr, EhdrT>::value || std::is_same<Elf32_Ehdr, EhdrT>::value,
                  "must be Elf{32,64}_Ehdr");
    static_assert(std::is_same<Elf64_Shdr, ShdrT>::value || std::is_same<Elf32_Shdr, ShdrT>::value,
                  "must be Elf{32,64}_Shdr");

    EhdrT elf_header{};

    ifs.seekg(0, std::ifstream::beg);
    ifs.read(reinterpret_cast<char*>(&elf_header), sizeof(elf_header));

    if (!ifs) {
        log_error("failed to read ELF header\n");
        return -1;
    }

    swap_data_if_necessary(elf_header, elf_header.e_shoff);
    swap_data_if_necessary(elf_header, elf_header.e_shentsize);
    swap_data_if_necessary(elf_header, elf_header.e_shnum);
    swap_data_if_necessary(elf_header, elf_header.e_shnum);

    off_t last_shdr_offset = elf_header.e_shoff + (elf_header.e_shentsize * (elf_header.e_shnum - 1));
    ShdrT section_header{};

    ifs.seekg(last_shdr_offset, std::ifstream::beg);
    ifs.read(reinterpret_cast<char*>(&section_header), sizeof(elf_header));

    if (!ifs) {
        log_error("failed to read ELF section header\n");
        return -1;
    }

    swap_data_if_necessary(elf_header, section_header.sh_offset);
    swap_data_if_necessary(elf_header, section_header.sh_size);

    /* ELF ends either with the table of section headers (SHT) or with a section. */
    off_t sht_end = elf_header.e_shoff + (elf_header.e_shentsize * elf_header.e_shnum);
    off_t last_section_end = section_header.sh_offset + section_header.sh_size;
    return sht_end > last_section_end ? sht_end : last_section_end;
}

bool is_32bit_elf(std::ifstream& ifs) {
    if (!ifs) {
        log_error("failed to read e_ident from ELF file\n");
        return -1;
    }

    // for the beginning, we just need to read e_ident to determine ELF class (i.e., either 32-bit or 64-bit)
    // that way, we can decide which way to go
    // the easiest way is to just use the ELF API
    Elf64_Ehdr ehdr;

    ifs.read(reinterpret_cast<char*>(&ehdr.e_ident), EI_NIDENT);

    switch (ehdr.e_ident[EI_CLASS]) {
        case ELFCLASS32: {
            return true;
        }
        case ELFCLASS64: {
            return false;
        }
    }

    throw std::logic_error{"ELF binary is neither 32-bit nor 64-bit"};
}

bool is_32bit_elf(const std::string& filename) {
    std::ifstream ifs(filename);

    if (!ifs) {
        log_error("could not open file: %s\n", filename.c_str());
        return -1;
    }

    return is_32bit_elf(ifs);
}

bool is_statically_linked_elf(std::ifstream& ifs) {
    if (!ifs) {
        log_error("failed to read e_ident from ELF file\n");
        return -1;
    }

    ssize_t pt_dynamic_offset;

    if (is_32bit_elf(ifs)) {
        pt_dynamic_offset = get_pt_dynamic_offset<Elf32_Ehdr, Elf32_Shdr>(ifs);
    } else {
        pt_dynamic_offset = get_pt_dynamic_offset<Elf64_Ehdr, Elf64_Shdr>(ifs);
    }

    return pt_dynamic_offset != -1;
}

bool is_statically_linked_elf(const std::string& filename) {
    std::ifstream ifs(filename);

    if (!ifs) {
        log_error("could not open file: %s\n", filename.c_str());
        return -1;
    }

    return is_statically_linked_elf(ifs);
}

ssize_t elf_binary_size(const std::string& filename) {
    std::ifstream ifs(filename);

    if (!ifs) {
        log_error("could not open file\n");
        return -1;
    }

    if (is_32bit_elf(ifs)) {
        return get_elf_size<Elf32_Ehdr, Elf32_Shdr>(ifs);
    } else {
        return get_elf_size<Elf64_Ehdr, Elf64_Shdr>(ifs);
    }
}
07070100000004000081a400000000000000000000000168cf6940000002a7000000000000000000000000000000000000001800000000src/binfmt-bypass/elf.h#pragma once

#include <string>

/**
 * Check whether file is linked staticallly.
 * @param filename path to ELF file
 * @return true if file is statically linked, false otherwise
 */
bool is_statically_linked_elf(const std::string& filename);

/**
 * Calculate size of ELF binary. Useful e.g., to estimate the size of the runtime in an AppImage.
 * @param filename path to ELF file
 * @return size of ELF part in bytes
 */
ssize_t elf_binary_size(const std::string& filename);

/**
 * Check whether a given ELF file is a 32-bit binary.
 * @param filename path to ELF file
 * @return true if it's a 32-bit ELF, false otherwise
 */
bool is_32bit_elf(const std::string& filename);
07070100000005000081a400000000000000000000000168cf6940000004f4000000000000000000000000000000000000002900000000src/binfmt-bypass/fix-preload-library.sh#! /bin/bash

set -euo pipefail

# 2.17 ensures compatibility with CentOS 7 and beyond
# cf. https://gist.github.com/wagenet/35adca1a032cec2999d47b6c40aa45b1
glibc_ok_version="2.17"

find_too_new_symbols() {
    glibc_symbols=( "$(nm --dynamic --undefined-only --with-symbol-versions "$1" | grep "GLIBC_")" )

    for glibc_symbol in "${glibc_symbols[@]}"; do
        # shellcheck disable=SC2001
        glibc_symbol_version="$(sed 's|.*GLIBC_\([\.0-9]\+\)$|\1|' <<< "$glibc_symbol")"
        newest_glibc_symbol_version="$(echo -e "$glibc_ok_version\\n$glibc_symbol_version" | sort -V | tail -n1)"

        # make sure the newest version found is <= the one we define as ok
        if [[ "$newest_glibc_symbol_version" == "$glibc_ok_version" ]]; then
            return 1
        fi
    done

    return 0
}

for file in "$@"; do
    # obviously, this is a hack, but it should work well enough since we just need to do it for one single symbol from libdl
    patchelf --debug --clear-symbol-version dlsym "$file"

    nm_data="$(nm --dynamic --undefined-only --with-symbol-versions "$file")"

    if find_too_new_symbols "$file"; then
        echo "Error: found symbol version markers newer than $glibc_ok_version:"
        echo "$nm_data"
        exit 1
    fi
done
07070100000006000081a400000000000000000000000168cf69400000075c000000000000000000000000000000000000002700000000src/binfmt-bypass/interpreter_main.cpp// system headers
#include <cassert>
#include <unistd.h>
#include <vector>

// own headers
#include "logging.h"
#include "lib.h"

bool executableExists(const std::string& path) {
    if (access(path.c_str(), X_OK) != 0) {
        log_debug("executable %s does not exist\n", path.c_str());
        return false;
    }

    return true;
}

int main(int argc, char** argv) {
    log_debug("Welcome to AppImageLauncher's binfmt_misc interpreter!\n");

    if (argc <= 1) {
        log_message("Usage: %s <AppImage file> [args...]\n", argv[0]);
        return EXIT_CODE_FAILURE;
    }

    const std::string appImagePath = argv[1];
    std::vector<char*> args(&argv[2], &argv[argc]);

    // optimistic approach
    bool useAppImageLauncher = true;

    if (!executableExists(APPIMAGELAUNCHER_PATH)) {
        log_message(
            "AppImageLauncher not found at %s, launching AppImage directly: %s\n",
            APPIMAGELAUNCHER_PATH,
            appImagePath.c_str()
        );
        useAppImageLauncher = false;
    }

    if (getenv("APPIMAGELAUNCHER_DISABLE") != nullptr) {
        log_message(
            "APPIMAGELAUNCHER_DISABLE set, launching AppImage directly: %s\n",
            APPIMAGELAUNCHER_PATH,
            appImagePath.c_str()
        );
        useAppImageLauncher = false;
    }

    if (!useAppImageLauncher) {
        return bypassBinfmtAndRunAppImage(argv[1], args);
    }

    log_debug(
        "AppImageLauncher found at %s, launching AppImage %s with it\n",
        APPIMAGELAUNCHER_PATH,
        appImagePath.c_str()
    );

    // needs to be done in inverse order
    args.emplace(args.begin(), strdup(appImagePath.c_str()));
    args.emplace(args.begin(), strdup(APPIMAGELAUNCHER_PATH));

    const auto rv = execv(APPIMAGELAUNCHER_PATH, args.data());

    assert(rv == -1);
    log_error("execv(%s, ...) failed\n");
    return EXIT_CODE_FAILURE;
}
07070100000007000081a400000000000000000000000168cf694000002bd7000000000000000000000000000000000000001a00000000src/binfmt-bypass/lib.cpp// system headers
#include <cstdio>
#include <sys/mman.h>
#include <unistd.h>
#include <fcntl.h>
#include <wait.h>
#include <vector>
#include <memory.h>
#include <memory>
#include <stdexcept>
#include <cassert>
#include <filesystem>

// own headers
#include "elf.h"
#include "logging.h"
#include "lib.h"
#include "binfmt-bypass-preload.h"

#ifdef PRELOAD_LIB_NAME_32BIT
    #include "binfmt-bypass-preload_32bit.h"
#endif

#define EXIT_CODE_FAILURE 0xff

bool copy_and_patch_runtime(int fd, const char* const appimage_filename, const ssize_t elf_size) {
    // copy runtime header into memfd "file"
    {
        const auto realfd = open(appimage_filename, O_RDONLY);
        std::vector<char> buffer(elf_size);
        // TODO: check for errors
        read(realfd, buffer.data(), elf_size);
        write(fd, buffer.data(), elf_size);
        close(realfd);
    }

    // erase magic bytes
    lseek(fd, 8, SEEK_SET);
    char null_buf[]{0, 0, 0};
    write(fd, null_buf, 3);

    // TODO: handle errors properly
    return true;
}

#ifdef HAVE_MEMFD_CREATE

// if memfd_create is available, we should use it as it has a few important advantages over older solutions like
// shm_open or classic tempfiles

int create_memfd_with_patched_runtime(const char* const appimage_filename, const ssize_t elf_size) {
    // as we call exec() after fork() to create a child process (the parent keeps it alive, the child doesn't require
    // access anyway), we enable close-on-exec
    const auto memfd = memfd_create("runtime", MFD_CLOEXEC);

    if (memfd < 0) {
        log_error("memfd_create failed: %s\n", strerror(errno));
        return -1;
    }

    if (!copy_and_patch_runtime(memfd, appimage_filename, elf_size)) {
        log_error("failed to copy and patch runtime\n");
        close(memfd);
        return -1;
    }

    return memfd;
}

#else

// in case memfd_create is *not* available, we fall back to shm_open
// it requires a few more lines of code (e.g., changing permissions to limit access to the created file)
// also, we can't just

int create_shm_fd_with_patched_runtime(const char* const appimage_filename, const ssize_t elf_size) {
    // let's hope that mktemp returns a unique filename; if not, shm_open returns an error, thanks to O_EXCL
    // the file exists only for a fraction of a second normally, so the chances are not too bad
    char mktemp_template[] = "runtime-XXXXXX";
    const char* runtime_filename = mktemp(mktemp_template);

    if (runtime_filename[0] == '\0') {
        log_error("failed to create temporary filename\n");
        return -1;
    }

    // shm_open doesn't survive over exec(), so we _have to_ keep this process alive and create a child for the runtime
    // the good news is: we don't have to worry about setting flags to close-on-exec
    int writable_fd = shm_open(runtime_filename, O_RDWR | O_CREAT, 0700);

    if (writable_fd < 0) {
        log_error("shm_open failed (writable): %s\n", strerror(errno));
        return -1;
    }

    // open file read-only before unlinking the file, this is the fd we return later
    // otherwise we'll end up with ETXTBSY when trying to exec() it
    int readable_fd = shm_open(runtime_filename, O_RDONLY, 0);

    if (readable_fd < 0) {
        log_error("shm_open failed (read-only): %s\n", strerror(errno));
        return -1;
    }

    // let's make sure the file goes away when it's closed
    // as long as we don't close the fd, it won't go away, but if we do, the OS takes care of freeing the memory
    if (shm_unlink(runtime_filename) != 0) {
        log_error("shm_unlink failed: %s\n", strerror(errno));
        close(writable_fd);
        return -1;
    }

    if (!copy_and_patch_runtime(writable_fd, appimage_filename, elf_size)) {
        log_error("failed to copy and patch runtime\n");
        close(writable_fd);
        return -1;
    }

    // close writable fd and return readable one
    close(writable_fd);
    return readable_fd;
}

#endif

std::filesystem::path find_preload_library(bool is_32bit) {
    // packaging is now done using ld-p-native_packages which does not make guarantees about the install path
    // therefore, we need to look up the path of the preload library in relation to the current binary's path
    // since we use the F (fix binary) binfmt mode nowadays to enable the use of the interpreter in different cgroups,
    // namespaces or changeroots, we may not find the library there, but we'll at least try

    // we expect the library to be placed next to this binary
    const auto own_binary_path = std::filesystem::read_symlink("/proc/self/exe");
    const auto dir_path = own_binary_path.parent_path();

    std::filesystem::path rv = dir_path;

#ifdef PRELOAD_LIB_NAME_32BIT
    if (is_32bit) {
        rv /= PRELOAD_LIB_NAME_32BIT;
        return rv;
    }
#endif

    rv /= PRELOAD_LIB_NAME;
    return rv;
}

/**
 * Create a temporary file within the shm file system and maintain its existence using the RAII principle.
 * This is a first attempt, creating the files within /tmp. Future versions could try to put the files next to the
 * AppImage, use a reproducible path to create the lib file just once, use shm_open etc.
 */
class TemporaryPreloadLibFile {
public:
    TemporaryPreloadLibFile(const unsigned char* libContents, const std::streamsize libContentsSize) {
        char tempFilePattern[] = "/tmp/appimagelauncher-preload-XXXXXX.so";

        _fd = mkstemps(tempFilePattern, 3);
        if (_fd == -1) {
            throw std::runtime_error("could not create temporary preload lib file");
        }

        _path = tempFilePattern;

        if (write(_fd, libContents, libContentsSize) != libContentsSize) {
            throw std::runtime_error("failed to write contents to temporary preload lib");
        }
    }

    ~TemporaryPreloadLibFile() {
        close(_fd);
        unlink(_path.c_str());
    };

    std::string path() {
        return _path;
    }

private:
    int _fd;
    std::filesystem::path _path;
};

// need to keep track of the subprocess pid in a global variable, as signal handlers in C(++) are simple interrupt
// handlers that are not aware of any state in main()
// note that we only connect the signal handler once we have created a subprocess, i.e., we don't need to worry about
// subprocess_pid not being set yet
// it's best practice to implement a check anyway, though
static pid_t subprocess_pid = 0;

void forwardSignal(int signal) {
    if (subprocess_pid != 0) {
        log_debug("forwarding signal %d to subprocess %ld\n", signal, subprocess_pid);
        kill(subprocess_pid, signal);
    } else {
        log_error("signal %d received but no subprocess created yet, shutting down\n", signal);
        exit(signal);
    }
}

int bypassBinfmtAndRunAppImage(const std::string& appimage_path, const std::vector<char*>& target_args) {
    // read size of AppImage runtime (i.e., detect size of ELF binary)
    const auto size = elf_binary_size(appimage_path.c_str());

    if (size < 0) {
        log_error("failed to detect runtime size\n");
        return EXIT_CODE_FAILURE;
    }

#ifdef HAVE_MEMFD_CREATE
    // create "file" in memory, copy runtime there and patch out magic bytes
    int runtime_fd = create_memfd_with_patched_runtime(appimage_path.c_str(), size);
#else
    int runtime_fd = create_shm_fd_with_patched_runtime(appimage_filename.c_str(), size);
#endif

    if (runtime_fd < 0) {
        log_error("failed to set up in-memory file with patched runtime\n");
        return EXIT_CODE_FAILURE;
    }

    // to keep alive the memfd, we launch the AppImage as a subprocess
    if ((subprocess_pid = fork()) == 0) {
        // create new argv array, using passed filename as argv[0]
        std::vector<char*> new_argv;

        new_argv.push_back(strdup(appimage_path.c_str()));

        // insert remaining args, if any
        for (const auto& arg : target_args) {
            new_argv.push_back(strdup(arg));
        }

        // needs to be null terminated, of course
        new_argv.push_back(nullptr);

        // preload our library
        auto preload_lib_path = find_preload_library(is_32bit_elf(appimage_path));

        log_debug("preload lib path: %s\n", preload_lib_path.string().c_str());

        // may or may not be used, but must survive until this application terminates
        std::unique_ptr<TemporaryPreloadLibFile> temporaryPreloadLibFile;

        if (!std::filesystem::exists(preload_lib_path)) {
            log_warning("could not find preload library, creating new temporary file for it\n");

#ifdef PRELOAD_LIB_NAME_32BIT
            if (is_32bit_elf(appimage_path)) {
                temporaryPreloadLibFile = std::make_unique<TemporaryPreloadLibFile>(
                    libbinfmt_bypass_preload_32bit_so,
                    libbinfmt_bypass_preload_32bit_so_len
                );
            }
#endif

            if (temporaryPreloadLibFile == nullptr) {
                temporaryPreloadLibFile = std::make_unique<TemporaryPreloadLibFile>(
                    libbinfmt_bypass_preload_so,
                    libbinfmt_bypass_preload_so_len
                );
            }

            assert(temporaryPreloadLibFile != nullptr);

            preload_lib_path = temporaryPreloadLibFile->path();
        }

        if (!is_statically_linked_elf(appimage_path)) {
            log_debug("library to preload: %s\n", preload_lib_path.string().c_str());
            setenv("LD_PRELOAD", preload_lib_path.c_str(), true);
        }

        // calculate absolute path to AppImage, for use in the preloaded lib
        char* abs_appimage_path = realpath(appimage_path.c_str(), nullptr);
        log_debug("absolute AppImage path: %s\n", abs_appimage_path);
        // TARGET_APPIMAGE is further needed for static runtimes which do not make any use of LD_PRELOAD
        setenv("REDIRECT_APPIMAGE", abs_appimage_path, true);
        setenv("TARGET_APPIMAGE", abs_appimage_path, true);

        // launch memfd directly, no path needed
        log_debug("fexecve(...)\n");
        fexecve(runtime_fd, new_argv.data(), environ);

        log_error("failed to execute patched runtime: %s\n", strerror(errno));
        return EXIT_CODE_FAILURE;
    }

    // now that we have a subprocess and know its process ID, it's time to set up signal forwarding
    // note that from this point on, we don't handle signals ourselves any more, but rely on the subprocess to exit
    // properly
    for (int i = 0; i < 32; ++i) {
        signal(i, forwardSignal);
    }

    // wait for child process to exit, and exit with its return code
    int status;
    wait(&status);

    // clean up
    close(runtime_fd);

    // calculate return code based on child's behavior
    int child_retcode;

    if (WIFSIGNALED(status) != 0) {
        child_retcode = WTERMSIG(status);
        log_error("child exited with code %d\n", child_retcode);
    } else if (WIFEXITED(status) != 0) {
        child_retcode = WEXITSTATUS(status);
        log_debug("child exited normally with code %d\n", child_retcode);
    } else {
        log_error("unknown error: child didn't exit with signal or regular exit code\n");
        child_retcode = EXIT_CODE_FAILURE;
    }

    return child_retcode;
}
07070100000008000081a400000000000000000000000168cf6940000000ce000000000000000000000000000000000000001800000000src/binfmt-bypass/lib.h#pragma once

// system headers
#include <string>
#include <vector>

#define EXIT_CODE_FAILURE 0xff

int bypassBinfmtAndRunAppImage(const std::string& appimage_path, const std::vector<char*>& target_args);
07070100000009000081a400000000000000000000000168cf694000000679000000000000000000000000000000000000001c00000000src/binfmt-bypass/logging.h#pragma once

// system headers
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#ifndef COMPONENT_NAME
#error component name undefined
#endif

static int v_log_message(const char* const format, va_list args) {
    static const char prefix[] = "[appimagelauncher-binfmt-bypass/" COMPONENT_NAME "] ";

    char* patched_format = (char*) (malloc(strlen(format) + strlen(prefix) + 1));
    strcpy(patched_format, prefix);
    strcat(patched_format, format);

    return vfprintf(stderr, patched_format, args);
}

static int v_log_message_prefix(const char* const prefix, const char* const format, va_list args) {
    char* patched_format = (char*) (malloc(strlen(format) + strlen(prefix) + 2 + 1));
    strcpy(patched_format, prefix);
    strcat(patched_format, ": ");
    strcat(patched_format, format);

    return v_log_message(patched_format, args);
}

static int log_message(const char* const format, ...) {
    va_list args;
    va_start(args, format);

    int result = v_log_message(format, args);

    va_end(args);

    return result;
}

static void log_debug(const char* const format, ...) {
    if (getenv("DEBUG") == NULL) {
        return;
    }

    va_list args;
    va_start(args, format);

    v_log_message_prefix("DEBUG", format, args);

    va_end(args);
}

static void log_error(const char* const format, ...) {
    va_list args;
    va_start(args, format);

    v_log_message_prefix("ERROR", format, args);

    va_end(args);
}


static void log_warning(const char* const format, ...) {
    va_list args;
    va_start(args, format);

    v_log_message_prefix("WARNING", format, args);

    va_end(args);
}
0707010000000a000081a400000000000000000000000168cf6940000011dd000000000000000000000000000000000000001c00000000src/binfmt-bypass/preload.c// system headers
#include <stdio.h>
#include <dlfcn.h>
#include <unistd.h>
#include <memory.h>
#include <stdlib.h>
#include <fcntl.h>
#include <limits.h>
#include <errno.h>
#include <stdbool.h>

// own headers
#include "logging.h"

// saw this trick somewhere on the Internet... don't recall where it was, but it works well
#ifndef RTLD_NEXT
#define RTLD_NEXT ((void*) -1l)
#endif

#define REAL_LIBC RTLD_NEXT

// TODO: move into central header, it's the same value in main.cpp
#define EXIT_CODE_FAILURE 0xff

// pointers to actual implementations in libc
// will be initialized by __init()
// TODO: create a macro for this pattern (DRY)
static char* (*__libc_realpath)(const char*, char*) = NULL;
static int (*__libc_open)(const char*, int) = NULL;
static ssize_t (*__libc_readlink)(const char*, void*, size_t) = NULL;

// TODO: write __init() and call that from all functions, loading all required symbols, the AppImage path etc. once
// to improve performance

// DRY
static const char proc_self_exe[] = "/proc/self/exe";


void __init() {
    static bool initialized = false;

    if (!initialized) {
        initialized = true;

        // get rid of $LD_PRELOAD in the first binary which this library is preloaded into (should be the runtime)
        // the easiest way is to wait for one of these functions to be used, then unset it
        unsetenv("LD_PRELOAD");

        // load symbols from libc
        __libc_readlink = (ssize_t (*) (const char*, void*, size_t)) dlsym(REAL_LIBC, "readlink");
        __libc_realpath = (char* (*) (const char*, char*)) dlsym(REAL_LIBC, "realpath");
        __libc_open = (int (*) (const char*, int)) dlsym(REAL_LIBC, "open");

        if (__libc_readlink == NULL || __libc_realpath == NULL || __libc_open == NULL) {
            log_error("failed to load symbol from libc\n");
            exit(EXIT_CODE_FAILURE);
        }
    }
}

char* __abs_appimage_path() {
    __init();

    static const char env_var_name[] = "TARGET_APPIMAGE";

    char* appimage_var = getenv(env_var_name);

    if (appimage_var == NULL || appimage_var[0] == '\0') {
        log_error("$%s not set\n", env_var_name);
        exit(EXIT_CODE_FAILURE);
    }

    // make path absolute if needed (best effort, it's better to pass an absolute value)
    if (appimage_var[0] != '/') {
        log_warning("$%s value is not absolute, trying to make it absolute\n", env_var_name);

        char* abspath = calloc(PATH_MAX, sizeof(char));

        if (readlink(appimage_var, abspath, sizeof(abspath)) == -1) {
            log_error("readlink failed on %s: %s\n", appimage_var, strerror(errno));
            exit(EXIT_CODE_FAILURE);
        }

        return abspath;
    }

    return strdup(appimage_var);
}

__attribute__((visibility ("default")))
extern ssize_t readlink(const char* path, char* buf, size_t len) {
    __init();

    log_debug("readlink %s, len %ld\n", path, len);

    if (strncmp(path, proc_self_exe, strlen(proc_self_exe)) == 0) {
        char* abspath = __abs_appimage_path();

        log_debug("redirecting readlink to %s\n", abspath);

        size_t ret = strlen(abspath);

        strncpy(buf, abspath, ret);

        log_debug("buf: %s, len: %ld\n", buf, ret);

        free(abspath);
        return ret;
    }

    return __libc_readlink(path, buf, len);
}

__attribute__((visibility ("default")))
extern char* realpath(const char* name, char* resolved) {
    __init();

    log_debug("realpath %s, %s\n", name, resolved);

    if (strncmp(name, proc_self_exe, strlen(proc_self_exe)) == 0) {
        char* appimage = __abs_appimage_path();

        log_debug("changing realpath destination to %s\n", appimage);

        if (resolved == NULL) {
            resolved = appimage;
        } else {
            strncpy(resolved, appimage, PATH_MAX);
            free(appimage);
        }

        return resolved;
    }

    char* retval = __libc_realpath(name, resolved);

    log_debug("realpath result: %s -> %s, retval %s\n", name, resolved, retval);

    return retval;
}

// used by squashfuse, specifically util.c/sqfs_fd_open
__attribute__((visibility ("default")))
extern int open(const char* file, int flags, ...) {
    __init();

    log_debug("open(%s, %d)\n", file, flags);

    char* abspath = NULL;

    if (strncmp(file, proc_self_exe, strlen(proc_self_exe)) == 0) {
        abspath = __abs_appimage_path();
        log_debug("redirecting open to %s\n", abspath);
        file = abspath;
    }

    int result = __libc_open(file, flags);

    if (abspath != NULL) {
        free(abspath);
    }

    return result;
}
0707010000000b000041ed00000000000000000000000168cf694000000000000000000000000000000000000000000000001200000000src/binfmt-bypass0707010000000c000081a400000000000000000000000168cf69400000013e000000000000000000000000000000000000001700000000src/cli/CMakeLists.txtadd_subdirectory(logging)

add_subdirectory(commands)

add_executable(ail-cli cli_main.cpp)
target_link_libraries(ail-cli PUBLIC cli_commands cli_logging)

set_property(
    TARGET ail-cli
    PROPERTY INSTALL_RPATH ${_rpath}
)

install(
    TARGETS ail-cli
    DESTINATION ${_bindir} COMPONENT APPIMAGELAUNCHER_CLI
)
0707010000000d000081a400000000000000000000000168cf6940000008a0000000000000000000000000000000000000001500000000src/cli/cli_main.cpp// system headers
#include <sstream>

// library headers
#include <QCoreApplication>
#include <QCommandLineParser>

// local headers
#include "CommandFactory.h"
#include "exceptions.h"
#include "logging.h"

using namespace appimagelauncher::cli;
using namespace appimagelauncher::cli::commands;

int main(int argc, char** argv) {
    // we don't have support for UI (and don't want that), so let's tell shared to not display dialogs
    setenv("_FORCE_HEADLESS", "1", 1);

    QCoreApplication app(argc, argv);

    std::ostringstream version;
    version << "version " << APPIMAGELAUNCHER_VERSION << " "
            << "(git commit " << APPIMAGELAUNCHER_GIT_COMMIT << "), built on "
            << APPIMAGELAUNCHER_BUILD_DATE;
    QCoreApplication::setApplicationVersion(QString::fromStdString(version.str()));

    QCommandLineParser parser;

    parser.addPositionalArgument("<command>", "Command to run (see help for more information");
    parser.addPositionalArgument("[...]", "command-specific additional arguments");
    parser.addHelpOption();
    parser.addVersionOption();

    parser.process(app);

    auto posArgs = parser.positionalArguments();

    if (posArgs.isEmpty()) {
        qerr() << parser.helpText().toStdString().c_str() << endl;

        qerr() << "Available commands:" << endl;
        qerr() << "  integrate        Integrate AppImages passed as commandline arguments" << endl;
        qerr() << "  unintegrate      Unintegrate AppImages passed as commandline arguments" << endl;
        qerr() << "  would-integrate  Report whether AppImage would be integrated (exits with 0 if yes, any other code if not)" << endl;

        return 2;
    }

    auto commandName = posArgs.front();
    posArgs.pop_front();

    try {
        auto command = CommandFactory::getCommandByName(commandName);
        command->exec(posArgs);
    } catch (const CommandNotFoundError& e) {
        qerr() << e.what() << endl;
        return 1;
    } catch (const InvalidArgumentsError& e) {
        qerr() << "Invalid arguments: " << e.what() << endl;
        return 3;
    } catch (const UsageError& e) {
        qerr() << "Usage error: " << e.what() << endl;
        return 3;
    }

    return 0;
}
0707010000000e000081a400000000000000000000000168cf69400000019b000000000000000000000000000000000000002000000000src/cli/commands/CMakeLists.txtadd_library(cli_commands STATIC
    Command.h
    CommandFactory.h CommandFactory.cpp
    exceptions.h
    IntegrateCommand.h IntegrateCommand.cpp
    UnintegrateCommand.h UnintegrateCommand.cpp
    WouldIntegrateCommand.h WouldIntegrateCommand.cpp
)
target_link_libraries(cli_commands PUBLIC Qt5::Core shared cli_logging libappimage)
target_include_directories(cli_commands PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
0707010000000f000081a400000000000000000000000168cf6940000001cc000000000000000000000000000000000000001b00000000src/cli/commands/Command.h#pragma once

// system headers
#include <memory>

// library headers
#include <QList>
#include <QString>

namespace appimagelauncher {
    namespace cli {
        namespace commands {
            /**
             * Base class for CLI command implementations.
             */
            class Command {
            public:
                // Run the command.
                virtual void exec(QList<QString> arguments) = 0;
            };
        }
    }
}


07070100000010000081a400000000000000000000000168cf694000000346000000000000000000000000000000000000002400000000src/cli/commands/CommandFactory.cpp// local headers
#include "CommandFactory.h"
#include "IntegrateCommand.h"
#include "UnintegrateCommand.h"
#include "WouldIntegrateCommand.h"
#include "exceptions.h"


namespace appimagelauncher {
    namespace cli {
        namespace commands {
            std::shared_ptr<Command> CommandFactory::getCommandByName(const QString& commandName) {
                if (commandName == "integrate") {
                    return std::shared_ptr<Command>(new IntegrateCommand);
                } else if (commandName == "unintegrate") {
                    return std::make_shared<UnintegrateCommand>();
                } else if (commandName == "would-integrate") {
                    return std::make_shared<WouldIntegrateCommand>();
                }

                throw CommandNotFoundError(commandName);
            }
        }
    }
}
07070100000011000081a400000000000000000000000168cf694000000197000000000000000000000000000000000000002200000000src/cli/commands/CommandFactory.h#pragma once

// system headers
#include <memory>

// library headers
#include <QString>

// local headers
#include <Command.h>

/**
 * Creates Commands.
 */

namespace appimagelauncher {
    namespace cli {
        namespace commands {
            class CommandFactory {
            public:
                static std::shared_ptr<Command> getCommandByName(const QString&);
            };
        }
    }
}
07070100000012000081a400000000000000000000000168cf694000000ee6000000000000000000000000000000000000002600000000src/cli/commands/IntegrateCommand.cpp// library headers
#include <QFileInfo>

// local headers
#include "IntegrateCommand.h"
#include "exceptions.h"
#include "shared.h"
#include "logging.h"

namespace appimagelauncher {
    namespace cli {
        namespace commands {
            void IntegrateCommand::exec(QList<QString> arguments) {
                if (arguments.empty()) {
                    throw InvalidArgumentsError("No AppImages passed on commandline");
                }

                // make sure all AppImages exist on disk before further processing
                for (auto& path : arguments) {
                    if (!QFileInfo(path).exists()) {
                        throw UsageError("could not find file " + path);
                    }

                    // make path absolute
                    // that will just prevent mistakes in libappimage and shared etc.
                    // (stuff like TryExec keys etc. being set to paths relative to CWD when running the command , ...)
                    path = QFileInfo(path).absoluteFilePath();
                }

                for (const auto& pathToAppImage : arguments) {
                    qout() << "Processing " << pathToAppImage << endl;

                    if (!QFileInfo(pathToAppImage).isFile()) {
                        qerr() << "Warning: Not a file, skipping: " << pathToAppImage << endl;
                        continue;
                    }

                    if (!isAppImage(pathToAppImage)) {
                        qerr() << "Warning: Not an AppImage, skipping: " << pathToAppImage << endl;
                        continue;
                    }

                    if (hasAlreadyBeenIntegrated(pathToAppImage)) {
                        if (desktopFileHasBeenUpdatedSinceLastUpdate(pathToAppImage)) {
                            qout() << "AppImage has been integrated already and doesn't need to be re-integrated, skipping" << endl;
                            continue;
                        }

                        qout() << "AppImage has already been integrated, but needs to be reintegrated" << endl;
                    }

                    auto pathToIntegratedAppImage = buildPathToIntegratedAppImage(pathToAppImage);

                    // make sure integration directory exists
                    // (important for new installations)
                    // pretty ugly, but well, one taketh what the Qt API giveth
                    QDir().mkdir(integratedAppImagesDestination().path());

                    // check if it's already in the right place
                    if (QFileInfo(pathToAppImage).absoluteFilePath() != QFileInfo(pathToIntegratedAppImage).absoluteFilePath()) {
                        qout() << "Moving AppImage to integration directory" << endl;

                        if (QFile::exists(pathToIntegratedAppImage) && !QFile(pathToIntegratedAppImage).remove()) {
                            qerr() << "Could not move AppImage into integration directory (error: failed to overwrite existing file)" << endl;
                            continue;
                        }

                        if (!QFile(pathToAppImage).rename(pathToIntegratedAppImage)) {
                            qerr() << "Cannot move AppImage to integration directory (permission problem?), attempting to copy instead" << endl;

                            if (!QFile(pathToAppImage).copy(pathToIntegratedAppImage)) {
                                throw CliError("Failed to copy AppImage, giving up");
                            }
                        }
                    } else {
                        qout() << "AppImage already in integration directory" << endl;
                    }

                    installDesktopFileAndIcons(pathToIntegratedAppImage);
                }
            }
        }
    }
}
07070100000013000081a400000000000000000000000168cf694000000181000000000000000000000000000000000000002400000000src/cli/commands/IntegrateCommand.h#pragma once

// local headers
#include "Command.h"

namespace appimagelauncher {
    namespace cli {
        namespace commands {
            /**
             * Integrates AppImages passed as arguments on the commandline.
             */
            class IntegrateCommand : public Command {
                void exec(QList<QString> arguments) final;
            };
        }
    }
}
07070100000014000081a400000000000000000000000168cf69400000079a000000000000000000000000000000000000002800000000src/cli/commands/UnintegrateCommand.cpp// library headers
#include <QFileInfo>

// local headers
#include "UnintegrateCommand.h"
#include "exceptions.h"
#include "shared.h"
#include "logging.h"

namespace appimagelauncher {
    namespace cli {
        namespace commands {
            void UnintegrateCommand::exec(QList<QString> arguments) {
                if (arguments.empty()) {
                    throw InvalidArgumentsError("No AppImages passed on commandline");
                }

                // make sure all AppImages exist on disk before further processing
                for (auto& path : arguments) {
                    if (!QFileInfo(path).exists()) {
                        throw UsageError("could not find file " + path);
                    }

                    // make path absolute
                    // that will just prevent mistakes in libappimage and shared etc.
                    // (stuff like TryExec keys etc. being set to paths relative to CWD when running the command , ...)
                    path = QFileInfo(path).absoluteFilePath();
                }

                for (const auto& pathToAppImage : arguments) {
                    qout() << "Processing " << pathToAppImage << endl;

                    if (!QFileInfo(pathToAppImage).isFile()) {
                        qerr() << "Warning: Not a file, skipping: " << pathToAppImage << endl;
                        continue;
                    }

                    if (!isAppImage(pathToAppImage)) {
                        qerr() << "Warning: Not an AppImage, skipping: " << pathToAppImage << endl;
                        continue;
                    }

                    if (!hasAlreadyBeenIntegrated(pathToAppImage)) {
                        qout() << "AppImage has not been integrated yet, skipping" << endl;
                        continue;
                    }

                    unregisterAppImage(pathToAppImage);
                }
            }
        }
    }
}
07070100000015000081a400000000000000000000000168cf694000000183000000000000000000000000000000000000002600000000src/cli/commands/UnintegrateCommand.h#pragma once

// local headers
#include "Command.h"

namespace appimagelauncher {
    namespace cli {
        namespace commands {
            /**
             * Integrates AppImages passed as arguments on the commandline.
             */
            class UnintegrateCommand : public Command {
                void exec(QList<QString> arguments) final;
            };
        }
    }
}
07070100000016000081a400000000000000000000000168cf694000000e5b000000000000000000000000000000000000002b00000000src/cli/commands/WouldIntegrateCommand.cpp// library headers
#include <QFileInfo>

extern "C" {
#include <appimage/appimage.h>
}

// local headers
#include "WouldIntegrateCommand.h"
#include "exceptions.h"
#include "shared.h"
#include "logging.h"

namespace appimagelauncher {
    namespace cli {
        namespace commands {
            void WouldIntegrateCommand::exec(QList<QString> arguments) {
                if (arguments.empty()) {
                    throw InvalidArgumentsError("No AppImages passed on commandline");
                }

                // make sure all AppImages exist on disk before further processing
                for (auto& path : arguments) {
                    if (!QFileInfo(path).exists()) {
                        throw UsageError("could not find file " + path);
                    }

                    // make path absolute
                    // that will just prevent mistakes in libappimage and shared etc.
                    // (stuff like TryExec keys etc. being set to paths relative to CWD when running the command , ...)
                    path = QFileInfo(path).absoluteFilePath();
                }

                for (const auto& pathToAppImage : arguments) {
                    qout() << "Checking whether " << pathToAppImage << " should be integrated" << endl;

                    if (!QFileInfo(pathToAppImage).isFile()) {
                        qerr() << "Warning: Not a file, skipping: " << pathToAppImage << endl;
                        continue;
                    }

                    if (!isAppImage(pathToAppImage)) {
                        qerr() << "Warning: Not an AppImage, skipping: " << pathToAppImage << endl;
                        continue;
                    }

                    // TODO: refactor into a function that, e.g., returns an enum

                    if (hasAlreadyBeenIntegrated(pathToAppImage)) {
                        if (desktopFileHasBeenUpdatedSinceLastUpdate(pathToAppImage)) {
                            throw WouldNotIntegrateError("AppImage has been integrated already and doesn't need to be re-integrated");
                        }

                        qout() << "AppImage has already been integrated, but needs to be reintegrated" << endl;
                    }


                    // check for X-AppImage-Integrate=false
                    auto shallNotBeIntegrated = appimage_shall_not_be_integrated(pathToAppImage.toStdString().c_str());
                    if (shallNotBeIntegrated < 0) {
                        throw CliError("AppImageLauncher error: appimage_shall_not_be_integrated() failed (returned " + QString::number(shallNotBeIntegrated) + ")");
                    } else if (shallNotBeIntegrated > 0) {
                        throw WouldNotIntegrateError("AppImage should not be integrated");
                    }

                    if (pathToAppImage.startsWith("/tmp/.mount_")) {
                        throw WouldNotIntegrateError("AppImages in AppImages are not supposed to be integrated");
                    }

                    // ignore terminal apps (fixes #2)
                    auto isTerminalApp = appimage_is_terminal_app(pathToAppImage.toStdString().c_str());
                    if (isTerminalApp < 0) {
                        throw CliError("AppImageLauncher error: appimage_is_terminal_app() failed (returned " + QString::number(isTerminalApp) + ")");
                    } else if (isTerminalApp > 0) {
                        throw WouldNotIntegrateError("Terminal AppImages should not be integrated");
                    }

                    qerr() << "AppImage should be integrated" << endl;
                }
            }
        }
    }
}
07070100000017000081a400000000000000000000000168cf694000000178000000000000000000000000000000000000002900000000src/cli/commands/WouldIntegrateCommand.h#pragma once

// local headers
#include "Command.h"

namespace appimagelauncher {
    namespace cli {
        namespace commands {
            /**
             * Check whether an AppImage would be integrated.
             */
            class WouldIntegrateCommand : public Command {
                void exec(QList<QString> arguments) final;
            };
        }
    }
}
07070100000018000081a400000000000000000000000168cf6940000004fc000000000000000000000000000000000000001e00000000src/cli/commands/exceptions.h#pragma once

// system headers
#include <stdexcept>

// library headers
#include <QString>

namespace appimagelauncher {
    namespace cli {
        namespace commands {
            class CliError : public std::runtime_error {
            public:
                explicit CliError(const QString& message) : std::runtime_error(message.toStdString()) {}
            };

            class CommandNotFoundError : public std::runtime_error {
            private:
                QString commandName;

            public:
                explicit CommandNotFoundError(const QString& commandName) : std::runtime_error(
                        "No such command available: " + commandName.toStdString()), commandName(commandName) {}

                QString getCommandName() const {
                    return commandName;
                }
            };

            class UsageError : public CliError {
            public:
                using CliError::CliError;
            };

            class InvalidArgumentsError : public UsageError {
            public:
                using UsageError::UsageError;
            };

            class WouldNotIntegrateError : public CliError {
            public:
                using CliError::CliError;
            };
        }
    }
}
07070100000019000041ed00000000000000000000000168cf694000000000000000000000000000000000000000000000001100000000src/cli/commands0707010000001a000081a400000000000000000000000168cf694000000087000000000000000000000000000000000000001f00000000src/cli/logging/CMakeLists.txtadd_library(cli_logging INTERFACE)
set_property(TARGET cli_logging PROPERTY INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR})
0707010000001b000081a400000000000000000000000168cf694000000111000000000000000000000000000000000000001a00000000src/cli/logging/logging.h#pragma once

// system headers
#include <string.h>

// library headers
#include <QTextStream>
#include <QDebug>

// wrapper for stdout
#define qout() QTextStream(stdout, QIODevice::WriteOnly)

// wrapper for stderr
#define qerr() QTextStream(stderr, QIODevice::WriteOnly)
0707010000001c000041ed00000000000000000000000168cf694000000000000000000000000000000000000000000000001000000000src/cli/logging0707010000001d000041ed00000000000000000000000168cf694000000000000000000000000000000000000000000000000800000000src/cli0707010000001e000081a400000000000000000000000168cf6940000001a1000000000000000000000000000000000000001a00000000src/daemon/CMakeLists.txt# daemon binary
add_executable(appimagelauncherd main.cpp daemon.cpp worker.cpp)
target_link_libraries(appimagelauncherd shared filesystemwatcher PkgConfig::glib libappimage)
set_target_properties(appimagelauncherd PROPERTIES INSTALL_RPATH ${_rpath})

install(
    TARGETS appimagelauncherd
    RUNTIME DESTINATION ${_bindir} COMPONENT APPIMAGELAUNCHER
    LIBRARY DESTINATION ${_libdir} COMPONENT APPIMAGELAUNCHER
)
0707010000001f000081a400000000000000000000000168cf6940000016b7000000000000000000000000000000000000001600000000src/daemon/daemon.cpp// STL headers
#include <chrono>

// library headers
#include <QDirIterator>

// local headers
#include "daemon.h"
#include "shared.h"
#include "appimage/appimage.h"

using namespace std::chrono_literals;

#define UPDATE_WATCHED_DIRECTORIES_INTERVAL 30s

namespace appimagelauncher::daemon {

    Q_LOGGING_CATEGORY(daemonCat, "appimagelauncher.daemon")

    Daemon::Daemon(QObject* parent) : QObject(parent), _settings(getConfig(this)), _worker(new Worker(this)),
                                      _watcher(new FileSystemWatcher(this)), _updateWatchedDirsTimer(new QTimer(this)) {
        // when we update the watched directories, the file system watcher can calculate whether there's new directories
        // to watch these
        QObject::connect(_watcher, &FileSystemWatcher::newDirectoriesToWatch, this, [this](const QDirSet& newDirs) {
            if (newDirs.empty()) {
                qCDebug(daemonCat) << "No new directories to watch detected";
            } else {
                qCInfo(daemonCat) << "Discovered new directories to watch, integrating existing AppImages initially";

                initialSearchForAppImages(newDirs);

                // (re-)integrate all AppImages at once
                _worker->executeDeferredOperations();
            }
        });


        // whenever a formerly watched directory disappears, we want to clean the menu from entries pointing to AppImages
        // in this directory
        // a good example for this situation is a removable drive that has been unplugged from the computer
        QObject::connect(_watcher, &FileSystemWatcher::directoriesToWatchDisappeared, this, [](const QDirSet& disappearedDirs) {
             if (disappearedDirs.empty()) {
                 qCDebug(daemonCat) << "No directories disappeared";
             } else {
                 qCInfo(daemonCat) << "Directories to watch disappeared, unintegrating AppImages formerly found in there";

                 if (!cleanUpOldDesktopIntegrationResources(true)) {
                     qCCritical(daemonCat) << "Error: Failed to clean up old desktop integration resources";
                 }
             }
         });

        // (re-)integrate all AppImages at once
        _worker->executeDeferredOperations();

        _updateWatchedDirsTimer->setInterval(UPDATE_WATCHED_DIRECTORIES_INTERVAL);
        connect(
            _updateWatchedDirsTimer, &QTimer::timeout, this,[this]() {
                _watcher->updateWatchedDirectories(this->watchedDirectories());
            }
        );
        _updateWatchedDirsTimer->start();


        connect(_watcher, &FileSystemWatcher::fileChanged, _worker, &Worker::scheduleForIntegration,
                Qt::QueuedConnection);
        connect(_watcher, &FileSystemWatcher::fileRemoved, _worker, &Worker::scheduleForUnintegration,
               Qt::QueuedConnection);
    }

    QDirSet Daemon::watchedDirectories() const {
        return daemonDirectoriesToWatch(_settings);
    }


    void Daemon::initialSearchForAppImages(const QDirSet& dirsToSearch) {
        // initial search for AppImages; if AppImages are found, they will be integrated, unless they already are
        qCInfo(daemonCat) << "Searching for existing AppImages";

        if (dirsToSearch.empty()) {
            qCWarning(daemonCat) << "No directories to search provided initially, skipping";
            return;
        }

        for (const auto& dir : dirsToSearch) {
            if (!dir.exists()) {
                qCDebug(daemonCat) << "Directory " << dir.path() << " does not exist, skipping";
                continue;
            }

            qCInfo(daemonCat) << "Searching directory: " << dir.absolutePath();

            for (QDirIterator it(dir); it.hasNext();) {
                const auto& path = it.next();

                if (QFileInfo(path).isFile()) {
                    const auto appImageType = appimage_get_type(path.toStdString().c_str(), false);
                    const auto isAppImage = 0 < appImageType && appImageType <= 2;

                    if (isAppImage) {
                        // at application startup, we don't want to integrate AppImages that have been integrated already,
                        // as that it slows down very much
                        // the integration will be updated as soon as any of these AppImages is run with AppImageLauncher
                        qCInfo(daemonCat) << "Found AppImage: " << path;

                        if (!appimage_is_registered_in_system(path.toStdString().c_str())) {
                            qCInfo(daemonCat) << "AppImage is not integrated yet, integrating";
                            _worker->scheduleForIntegration(path);
                        } else if (!desktopFileHasBeenUpdatedSinceLastUpdate(path)) {
                            qCInfo(daemonCat) << "AppImage has been integrated already but needs to be reintegrated";
                            _worker->scheduleForIntegration(path);
                        } else {
                            qCInfo(daemonCat) << "AppImage integrated already, skipping";
                        }
                    }
                }
            }
        }
    }

    bool Daemon::startWatching() {
        // make sure the watched directories list is up to date
        _watcher->updateWatchedDirectories(watchedDirectories());

        // search directories to watch once initially
        // we *have* to do this even though we connect this signal above, as the first update occurs in the constructor
        // and we cannot connect signals before construction has finished for obvious reasons
        initialSearchForAppImages(_watcher->directories());

        return _watcher->startWatching();
    }

    void Daemon::slotStopWatching() {
        _watcher->stopWatching();
    }

}
07070100000020000081a400000000000000000000000168cf694000000306000000000000000000000000000000000000001400000000src/daemon/daemon.h#pragma once

// library headers
#include <QObject>
#include <QSettings>
#include <QTimer>
#include <QLoggingCategory>

// local headers
#include "worker.h"
#include "types.h"
#include "filesystemwatcher.h"

namespace appimagelauncher::daemon {

    Q_DECLARE_LOGGING_CATEGORY(daemonCat)

    class Daemon : public QObject {
        Q_OBJECT

    public:
        explicit Daemon(QObject* parent = nullptr);
        QDirSet watchedDirectories() const;
        bool startWatching();

    public slots:
        void slotStopWatching();

    private:
        void initialSearchForAppImages(const QDirSet& dirsToSearch);

        QSettings *_settings;
        Worker* _worker;
        FileSystemWatcher *_watcher;

        QTimer* _updateWatchedDirsTimer;
    };

} // namespace
07070100000021000081a400000000000000000000000168cf6940000010e7000000000000000000000000000000000000001400000000src/daemon/main.cpp// system includes
#include <deque>
#include <iostream>
#include <sstream>
#include <sys/stat.h>

// library includes
#include <QCommandLineParser>
#include <QCoreApplication>
#include <QDebug>
#include <QDirIterator>
#include <QTimer>
#include <appimage/appimage.h>

// local includes
#include "shared.h"
#include "daemon.h"

using namespace appimagelauncher::daemon;

/**
 * Read the modification time of the file pointed by <filePath>
 * @param filePath
 * @return file modification time
 */
long readFileModificationTime(char* filePath) {
    struct stat attrib = {0x0};
    stat(filePath, &attrib);
    return attrib.st_ctime;
}

/**
 * Monitors whether the application binary has changed since the process was started. In such case the application
 * is restarted.
 *
 * @param argv
 */
QTimer* setupBinaryUpdatesMonitor(char* const* argv) {
    auto* timer = new QTimer();
    // It's used to restart the daemon if the binary changes
    static const long binaryModificationTime = readFileModificationTime(argv[0]);

    // callback to compare and restart the app if the binary changed since it was started
    QObject::connect(timer, &QTimer::timeout, [argv]() {
        long newBinaryModificationTime = readFileModificationTime(argv[0]);
        if (newBinaryModificationTime != binaryModificationTime) {
            std::cerr << "Binary file changed since the applications was started, proceeding to restart it."
                      << std::endl;
            execv(argv[0], argv);
        }
    });

    // check every 5 min
    timer->setInterval(5 * 60 * 1000);
    return timer;
}


int main(int argc, char* argv[]) {
    // make sure shared won't try to use the UI
    setenv("_FORCE_HEADLESS", "1", 1);

    // improve default logging format
    qSetMessagePattern("[%{type}] %{category}: %{message}");

    QCommandLineParser parser;
    parser.setApplicationDescription(
        QObject::tr(
            "Tracks AppImages in applications directories (user's, system and other ones). "
            "Automatically integrates AppImages moved into those directories and unintegrates ones removed from them."
        )
    );

    QCommandLineOption listWatchedDirectoriesOption(
        "list-watched-directories",
        QObject::tr("Lists directories watched by this daemon and exit")
    );

    if (!parser.addOption(listWatchedDirectoriesOption)) {
        throw std::runtime_error("could not add Qt command line option for some reason");
    }

    QCommandLineOption debugOption(
        "debug",
        QObject::tr("Enable debug logging")
    );

    if (!parser.addOption(debugOption)) {
        throw std::runtime_error("could not add Qt command line option for some reason");
    }

    QCoreApplication app(argc, argv);

    {
        std::ostringstream version;
        version << "version " << APPIMAGELAUNCHER_VERSION << " "
                << "(git commit " << APPIMAGELAUNCHER_GIT_COMMIT << "), built on "
                << APPIMAGELAUNCHER_BUILD_DATE;
        QCoreApplication::setApplicationVersion(QString::fromStdString(version.str()));
    }

    // parse arguments
    parser.process(app);

    if (!parser.isSet(debugOption)) {
        QLoggingCategory::setFilterRules("*.debug=false");
    } else {
        QLoggingCategory::setFilterRules("*.debug=true");
    }

    auto* daemon = new Daemon(&app);

    // this option is for debugging the
    if (parser.isSet(listWatchedDirectoriesOption)) {
        for (const auto& watchedDir : daemon->watchedDirectories()) {
            std::cout << watchedDir.absolutePath().toStdString() << std::endl;
        }
        return 0;
    }

    // after (re-)integrating all AppImages, clean up old desktop integration resources before start
    if (!cleanUpOldDesktopIntegrationResources()) {
        std::cout << "Failed to clean up old desktop integration resources" << std::endl;
    }

    qInfo() << "Watching directories:" << daemon->watchedDirectories();

    if (!daemon->startWatching()) {
        std::cerr << "Could not start watching directories" << std::endl;
        return 1;
    }

    QCoreApplication::connect(&app, &QCoreApplication::aboutToQuit, daemon, &Daemon::slotStopWatching);

    auto* binaryUpdatesMonitor = setupBinaryUpdatesMonitor(argv);
    binaryUpdatesMonitor->start();

    return QCoreApplication::exec();
}
07070100000022000081a400000000000000000000000168cf69400000194f000000000000000000000000000000000000001600000000src/daemon/worker.cpp// system includes
#include <atomic>
#include <iostream>
#include <deque>

// library includes
#include <QDebug>
#include <QFile>
#include <QObject>
#include <QSysInfo>
#include <QTimer>
#include <QThreadPool>
#include <QMutexLocker>
#include <appimage/appimage.h>

// local includes
#include "worker.h"
#include "shared.h"

namespace {

    enum OP_TYPE {
        INTEGRATE = 0,
        UNINTEGRATE = 1,
    };

    typedef std::pair<QString, OP_TYPE> Operation;

}

namespace appimagelauncher::daemon {

    Q_LOGGING_CATEGORY(workerCat, "appimagelauncher.daemon.worker")

    class Worker::PrivateData {
    public:
        QTimer deferredOperationsTimer;

        static constexpr int TIMEOUT = 15 * 1000;

        // std::set is unordered, therefore using std::deque to keep the order of the operations
        std::deque<Operation> deferredOperations;

        class OperationTask : public QRunnable {
        private:
            Operation operation;
            QMutex* mutex;

        public:
            OperationTask(const Operation& operation, QMutex* mutex) : operation(operation), mutex(mutex) {}

            void run() override {
                const auto& path = operation.first;
                const auto& type = operation.second;

                const auto exists = QFile::exists(path);
                const auto appImageType = appimage_get_type(path.toStdString().c_str(), false);
                const auto isAppImage = 0 < appImageType && appImageType <= 2;

                if (type == INTEGRATE) {
                    {   // Scope for Output Mutex Locker
                        QMutexLocker mutexLocker(mutex);
                        std::cout << "Integrating: " << path.toStdString() << std::endl;

                        if (!exists) {
                            std::cout << "ERROR: file does not exist, cannot integrate" << std::endl;
                            return;
                        }

                        if (!isAppImage) {
                            std::cout << "ERROR: not an AppImage, skipping" << std::endl;
                            return;
                        }
                    }

                    // check for X-AppImage-Integrate=false
                    if (appimage_shall_not_be_integrated(path.toStdString().c_str())) {
                        QMutexLocker mutexLocker(mutex);
                        std::cout << "WARNING: AppImage shall not be integrated, skipping" << std::endl;
                        return;
                    }

                    if (!installDesktopFileAndIcons(path)) {
                        QMutexLocker mutexLocker(mutex);
                        std::cout << "ERROR: Failed to register AppImage in system" << std::endl;
                        return;
                    }
                } else if (type == UNINTEGRATE) {
                    // nothing to do
                }
            }
        };

    public:
        PrivateData() {
            deferredOperationsTimer.setSingleShot(true);
            deferredOperationsTimer.setInterval(TIMEOUT);
        }

    public:
        // in addition to a simple duplicate check, this function is context sensitive
        // it starts with the last element, and checks for duplicates until an opposite action is found
        // for instance, when the element shall integrated, it will check for duplicates until an unintegration operation
        // is found
        bool isDuplicate(Operation operation) {
            for (auto it = deferredOperations.rbegin(); it != deferredOperations.rend(); ++it) {
                if ((*it).first == operation.first) {
                    // if operation type is different, then the operation is new, and should be added to the list
                    // if it is equal, it's a duplicate
                    // in either case, the loop can be aborted here
                    return (*it).second == operation.second;
                }
            }

            return false;
        }
    };

    Worker::Worker(QObject* parent) : QObject(parent) {
        d = std::make_shared<PrivateData>();

        connect(this, &Worker::startTimer, this, &Worker::startTimerIfNecessary, Qt::QueuedConnection);
        connect(&d->deferredOperationsTimer, &QTimer::timeout, this, &Worker::executeDeferredOperations);
    }

    void Worker::executeDeferredOperations() {
        if (d->deferredOperations.empty()) {
            qCDebug(workerCat) << "No deferred operations to execute";
            return;
        }

        std::cout << "Executing deferred operations" << std::endl;

        QMutex outputMutex;

        while (!d->deferredOperations.empty()) {
            auto operation = d->deferredOperations.front();
            d->deferredOperations.pop_front();
            QThreadPool::globalInstance()->start(new PrivateData::OperationTask(operation, &outputMutex));
        }

        // wait until all AppImages have been integrated
        QThreadPool::globalInstance()->waitForDone();

        std::cout << "Cleaning up old desktop integration files" << std::endl;
        if (!cleanUpOldDesktopIntegrationResources(true)) {
            std::cout << "Failed to clean up old desktop integration files" << std::endl;
        }

        // make sure the icons in the launcher are refreshed
        std::cout << "Updating desktop database and icon caches" << std::endl;
        if (!updateDesktopDatabaseAndIconCaches())
            std::cout << "Failed to update desktop database and icon caches" << std::endl;

        std::cout << "Done" << std::endl;
    }

    void Worker::scheduleForIntegration(const QString& path) {
        auto operation = std::make_pair(path, INTEGRATE);
        if (!d->isDuplicate(operation)) {
            std::cout << "Scheduling for (re-)integration: " << path.toStdString() << std::endl;
            d->deferredOperations.push_back(operation);
            emit startTimer();
        }

    }

    void Worker::scheduleForUnintegration(const QString& path) {
        auto operation = std::make_pair(path, UNINTEGRATE);
        if (!d->isDuplicate(operation)) {
            std::cout << "Scheduling for unintegration: " << path.toStdString() << std::endl;
            d->deferredOperations.push_back(operation);
            emit startTimer();
        }
    }

    void Worker::startTimerIfNecessary() {
        if (!d->deferredOperationsTimer.isActive())
            QMetaObject::invokeMethod(&d->deferredOperationsTimer, "start");
    }

}
07070100000023000081a400000000000000000000000168cf6940000002cd000000000000000000000000000000000000001400000000src/daemon/worker.h// system includes
#include <memory>

// library includes
#include <QObject>
#include <QLoggingCategory>

#pragma once

namespace appimagelauncher::daemon {

    Q_DECLARE_LOGGING_CATEGORY(workerCat)

    class Worker : public QObject {
        Q_OBJECT

    private:
        class PrivateData;
        std::shared_ptr<PrivateData> d = nullptr;

    public:
        explicit Worker(QObject* parent = nullptr);

    signals:
        void startTimer();

    public slots:
        void scheduleForIntegration(const QString& path);
        void scheduleForUnintegration(const QString& path);

    public slots:
        void executeDeferredOperations();

    private slots:
        void startTimerIfNecessary();
    };

}
07070100000024000041ed00000000000000000000000168cf694000000000000000000000000000000000000000000000000b00000000src/daemon07070100000025000081a400000000000000000000000168cf6940000000e2000000000000000000000000000000000000001d00000000src/fswatcher/CMakeLists.txtadd_library(filesystemwatcher STATIC filesystemwatcher.cpp filesystemwatcher.h)
target_link_libraries(filesystemwatcher PUBLIC Qt5::Core shared)
target_include_directories(filesystemwatcher PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
07070100000026000081a400000000000000000000000168cf694000002dad000000000000000000000000000000000000002400000000src/fswatcher/filesystemwatcher.cpp// system includes
#include <iostream>
#include <map>
#include <unistd.h>

// library includes
#include <QDir>
#include <QMutex>
#include <QTimer>
#include <QThread>
#include <sys/inotify.h>

// local includes
#include "filesystemwatcher.h"

namespace {

    class INotifyEvent {
    public:
        uint32_t mask;
        QString path;

    public:
        INotifyEvent(uint32_t mask, QString path) : mask(mask), path(std::move(path)) {}
    };

}


namespace appimagelauncher::daemon {

    Q_LOGGING_CATEGORY(fswCat, "appimagelauncher.daemon.filesystemwatcher")

    class FileSystemWatcher::PrivateData {
    public:
        enum EVENT_TYPES {
            // events that indicate file creations, modifications etc.
            fileChangeEvents = IN_CLOSE_WRITE | IN_MOVE,
            // events that indicate a file removal from a directory, e.g., deletion or moving to another location
            fileRemovalEvents = IN_DELETE | IN_MOVED_FROM,
        };

        // tracks whether the watcher is running
        bool isRunning;

    public:
        QDirSet watchedDirectories;
        QTimer eventsLoopTimer;
        QMutex* mutex;

    private:
        int inotifyFd = -1;
        std::map<int, QDir> watchFdMap;

    public:
        // reads events from the inotify fd and emits the correct signals
        std::vector<INotifyEvent> readEventsFromFd() {
            // we don't want to read events in parallel
            QMutexLocker lock{mutex};

            // read raw bytes into buffer
            // this is necessary, as the inotify_events have dynamic sizes
            static const auto bufSize = 4096;
            char buffer[bufSize] __attribute__ ((aligned(8)));

            const auto rv = read(inotifyFd, buffer, bufSize);
            const auto error = errno;

            if (rv == 0) {
                throw FileSystemWatcherError("read() on inotify FD must never return 0");
            }

            if (rv == -1) {
                // we're using a non-blocking inotify fd, therefore, if errno is set to EAGAIN, we just didn't find any
                // new events
                // this is not an error case
                if (error == EAGAIN)
                    return {};

                throw FileSystemWatcherError(QString("Failed to read from inotify fd: ") + strerror(error));
            }

            // read events into vector
            std::vector<INotifyEvent> events;

            for (char* p = buffer; p < buffer + rv;) {
                // create inotify_event from current position in buffer
                auto* currentEvent = (struct inotify_event*) p;

                // initialize new INotifyEvent with the data from the currentEvent
                QString relativePath(currentEvent->name);
                auto directory = watchFdMap[currentEvent->wd];
                events.emplace_back(currentEvent->mask, directory.absolutePath() + "/" + relativePath);

                // update current position in buffer
                p += sizeof(struct inotify_event) + currentEvent->len;
            }

            return events;
        }

        PrivateData() : isRunning(false), watchedDirectories(), mutex(new QMutex) {
            inotifyFd = inotify_init1(IN_NONBLOCK);

            if (inotifyFd < 0) {
                auto error = errno;
                throw FileSystemWatcherError(QString("Failed to initialize inotify, reason: ") + strerror(error));
            }
        };

        // caution: method is not threadsafe!
        bool startWatching(const QDir& directory) {
            static const auto mask = fileChangeEvents | fileRemovalEvents;

            qCDebug(fswCat) << "start watching directory " << directory;

            if (!directory.exists()) {
                qCDebug(fswCat) << "Warning: directory " << directory.absolutePath() << " does not exist, skipping";
                return true;
            }

            const int watchFd = inotify_add_watch(inotifyFd, directory.absolutePath().toStdString().c_str(), mask);

            if (watchFd == -1) {
                const auto error = errno;
                qCCritical(fswCat) << "Failed to start watching: " << strerror(error);
                return false;
            }

            watchFdMap[watchFd] = directory;
            eventsLoopTimer.start();

            return true;
        }

        bool startWatching() {
            QMutexLocker lock{mutex};

            for (const auto& directory: watchedDirectories) {
                if (!startWatching(directory))
                    return false;
            }

            return true;
        }

        bool startWatching(const QDirSet& directories) {
            QMutexLocker lock{mutex};

            for (const auto& directory: directories) {
                if (!startWatching(directory)) {
                    return false;
                }
            }

            return true;
        }

        // caution: method is not threadsafe!
        bool stopWatching(int watchFd) {
            // no matter whether the watch removal succeeds, retrying to remove the watch won't help
            // therefore, we can remove the file descriptor from the map in any case
            watchFdMap.erase(watchFd);

            qCDebug(fswCat) << "stop watching watchfd " << watchFd;

            if (inotify_rm_watch(inotifyFd, watchFd) == -1) {
                const auto error = errno;
                qCCritical(fswCat) << "Failed to stop watching: " << strerror(error);
                return false;
            }

            return true;
        }

        bool stopWatching() {
            QMutexLocker lock{mutex};

            while (!watchFdMap.empty()) {
                const auto pair = *(watchFdMap.begin());
                const auto watchFd = pair.first;

                if (!stopWatching(watchFd)) {
                    qCCritical(fswCat) << "Warning: Failed to stop watching on file descriptor " << watchFd;
                }
            }

            return true;
        }

        bool stopWatching(const QDirSet& directories) {
            QMutexLocker lock{mutex};

            for (const auto& directory: directories) {
                for (const auto& pair: watchFdMap) {
                    if (pair.second == directory) {
                        if (!stopWatching(pair.first)) {
                            return false;
                        }
                    }
                }

                // reaching the following line means that we couldn't find the requested path in the fd map
                return false;
            }

            return true;
        }
    };

    FileSystemWatcher::FileSystemWatcher(QObject* parent) : QObject(parent) {
        d = std::make_shared<PrivateData>();

        d->eventsLoopTimer.setInterval(100);
        connect(&d->eventsLoopTimer, &QTimer::timeout, this, &FileSystemWatcher::readEvents);
    }

    FileSystemWatcher::FileSystemWatcher(const QDir& path, QObject* parent) : FileSystemWatcher(parent) {
        updateWatchedDirectories(QDirSet{{path}});
    }

    FileSystemWatcher::FileSystemWatcher(const QDirSet& paths, QObject* parent) : FileSystemWatcher(parent) {
        updateWatchedDirectories(paths);
    }

    QDirSet FileSystemWatcher::directories() {
        QMutexLocker lock{d->mutex};

        return d->watchedDirectories;
    }

    bool FileSystemWatcher::startWatching() {
        {
            QMutexLocker lock{d->mutex};

            if (d->isRunning) {
                qCDebug(fswCat) << "tried to start file system watcher while it's running already";
                return true;
            }
        }

        auto rv = d->startWatching();

        {
            QMutexLocker lock{d->mutex};

            if (rv)
                d->isRunning = true;
        }

        return rv;
    }

    bool FileSystemWatcher::stopWatching() {
        {
            QMutexLocker lock{d->mutex};

            if (!d->isRunning) {
                qCDebug(fswCat) << "tried to stop file system watcher while stopped";
                return true;
            }
        }

        const auto rv = d->stopWatching();

        {
            QMutexLocker lock{d->mutex};

            if (rv) {
                d->isRunning = false;

                // we can stop reporting events now, I guess
                d->eventsLoopTimer.stop();
            }
        }

        return rv;
    }

    void FileSystemWatcher::readEvents() {
        auto events = d->readEventsFromFd();

        for (const auto& event: events) {
            const auto mask = event.mask;

            if (mask & d->fileChangeEvents) {
                emit fileChanged(event.path);
            } else if (mask & d->fileRemovalEvents) {
                emit fileRemoved(event.path);
            }
        }
    }

    bool FileSystemWatcher::updateWatchedDirectories(QDirSet watchedDirectories) {
        // the list may contain entries for directories which don't exist already, therefore we have to remove those first
        // so when they'll be created, we'll notice
        {
            // erase-remove doesn't work with sets apparently (see https://stackoverflow.com/a/26833313)
            // therefore we use a simple linear search to remove non-existing directories
            for (auto it = watchedDirectories.begin(); it != watchedDirectories.end(); ++it) {
                if (!it->exists()) {
                    qCDebug(fswCat) << "Directory " << it->path() << " does not exist, skipping";
                    it = watchedDirectories.erase(it);
                }
            }
        }

        auto setDifference = [](const QDirSet& toExamine, const QDirSet& toSearchFor) -> QDirSet {
            QDirSet results;

            // QDir behaves weirdly with STL algorithm comparisons etc.
            // therefore we implement this difference algorithm all by ourselves to make sure it works correctly
            for (const auto& examined: toExamine) {
                bool found = false;

                for (const auto& searched: toSearchFor) {
                    if (searched == examined) {
                        found = true;
                        break;
                    }
                }

                if (!found) {
                    results.insert(examined);
                }
            }

            return results;
        };

        // first, we calculate which directores are new to be watched
        QDirSet newDirectories = setDifference(watchedDirectories, d->watchedDirectories);

        // to stop watching with a fine granularity, we also need to know which directories have been removed
        QDirSet disappearedDirectories = setDifference(d->watchedDirectories, watchedDirectories);

        {
            QMutexLocker lock{d->mutex};

            // now we can update the internal state
            d->watchedDirectories = watchedDirectories;

            // if the watching hasn't been started yet, we shouldn't start/stop any watches
            // unfortunately we need an extra variable to track this...
            if (!d->isRunning)
                return true;
        }

        // we must run both stop and start methods, so we cannot directly return false if either fails
        // also, this makes sure the signals are sent even in case either of the following methods fails
        bool rv = true;
        rv = rv && d->stopWatching(disappearedDirectories);
        rv = rv && d->startWatching(newDirectories);

        // send out the signals for further handling by users of a fs watcher instance
        emit newDirectoriesToWatch(newDirectories);
        emit directoriesToWatchDisappeared(disappearedDirectories);

        return rv;
    }

}
07070100000027000081a400000000000000000000000168cf69400000055e000000000000000000000000000000000000002200000000src/fswatcher/filesystemwatcher.h// system includes
#include <algorithm>
#include <memory>
#include <unordered_set>

// library includes
#include <QDir>
#include <QObject>
#include <QLoggingCategory>
#include <QSet>
#include <QString>
#include <QThread>

// local includes
#include "types.h"

#pragma once

namespace appimagelauncher::daemon {

    Q_DECLARE_LOGGING_CATEGORY(fswCat)

    class FileSystemWatcherError : public std::runtime_error {
    public:
        explicit FileSystemWatcherError(const QString& message) : std::runtime_error(message.toStdString().c_str()) {};
    };

    class FileSystemWatcher : public QObject {
        Q_OBJECT

    private:
        class PrivateData;

        std::shared_ptr<PrivateData> d;

    public:
        explicit FileSystemWatcher(const QDir& directory, QObject* parent = nullptr);
        explicit FileSystemWatcher(const QDirSet& paths, QObject* parent = nullptr);
        explicit FileSystemWatcher(QObject* parent = nullptr);

    public slots:
        bool startWatching();
        bool stopWatching();
        void readEvents();
        bool updateWatchedDirectories(QDirSet watchedDirectories);

    public:
        QDirSet directories();

    signals:
        void fileChanged(QString path);
        void fileRemoved(QString path);
        void newDirectoriesToWatch(QDirSet set);
        void directoriesToWatchDisappeared(QDirSet set);
    };

}
07070100000028000041ed00000000000000000000000168cf694000000000000000000000000000000000000000000000000e00000000src/fswatcher07070100000029000081a400000000000000000000000168cf694000000117000000000000000000000000000000000000001800000000src/i18n/CMakeLists.txtadd_library(translationmanager translationmanager.cpp translationmanager.h)
target_link_libraries(translationmanager PUBLIC Qt5::Core Qt5::Widgets shared)
target_include_directories(translationmanager PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
add_dependencies(translationmanager l10n)
0707010000002a000081a400000000000000000000000168cf694000000a9e000000000000000000000000000000000000002000000000src/i18n/translationmanager.cpp// system headers
#include <iostream>

// library headers
#include <QDebug>
#include <QDir>
#include <QLibraryInfo>
#include <QString>

// local headers
#include <shared.h>
#include "translationmanager.h"

TranslationManager::TranslationManager(QCoreApplication& app) : app(app) {
    // set up translations
    auto qtTranslator = new QTranslator();
    qtTranslator->load("qt_" + QLocale::system().name(), QLibraryInfo::location(QLibraryInfo::TranslationsPath));
    app.installTranslator(qtTranslator);

    const auto systemLocale = QLocale::system().name();

    // we're using primarily short names for translations, so we should load these translations as well
    const auto shortSystemLocale = systemLocale.split('_')[0];

    const auto translationDir = getTranslationDir();

    auto myappTranslator = new QTranslator();
    myappTranslator->load(translationDir + "/ui." + systemLocale + ".qm");
    myappTranslator->load(translationDir + "/ui." + shortSystemLocale + ".qm");
    app.installTranslator(myappTranslator);

    // store translators in list so they won't be deleted
    installedTranslators.push_back(qtTranslator);
    installedTranslators.push_back(myappTranslator);
}

TranslationManager::~TranslationManager() {
    for (auto& translator : installedTranslators) {
        delete translator;
        translator = nullptr;
    }
}

QString TranslationManager::getTranslationDir() {
    // first we need to find the translation directory
    // if this is run from the build tree, we try a path that can only work within the build directory
    // then, we try the expected install location relative to the main binary
    const auto binaryDirPath = QApplication::applicationDirPath();

    // previously the path to the repo root dir was embedded to allow for finding the compiled translations
    // this lead to irreproducible builds
    // therefore the files are now generated within the build dir, and we guess the path based on the binary location
    auto translationDir = binaryDirPath + "/../../i18n/generated/l10n";

    // when the application is installed, we need to look for the files in the private data directory
    if (!QDir(translationDir).exists()) {
        auto privateDataDir = pathToPrivateDataDirectory();
        if (!privateDataDir.isEmpty()) {
            translationDir = privateDataDir + "/l10n";
        }
    }

    // give the user (and dev) some feedback whether the translations could actually be found or not
    if (!QDir(translationDir).exists()) {
        std::cerr << "[AppImageLauncher] Warning: "
                  << "Translation directory could not be found, translations are likely not available" << std::endl;
    }

    return translationDir;
}
0707010000002b000081a400000000000000000000000168cf694000000223000000000000000000000000000000000000001e00000000src/i18n/translationmanager.h#pragma once

// library includes
#include <QApplication>
#include <QTranslator>
#include <QList>

/*
 * Installs translations for AppImageLauncher UIs in a Qt application.
 *
 * You need to keep instances of this alive until the application is terminated.
 */
class TranslationManager {
private:
    const QCoreApplication& app;
    QList<QTranslator*> installedTranslators;

public:
    explicit TranslationManager(QCoreApplication& app);
    ~TranslationManager();

public:
    // get translation dir
    static QString getTranslationDir();
};
0707010000002c000041ed00000000000000000000000168cf694000000000000000000000000000000000000000000000000900000000src/i18n0707010000002d000081a400000000000000000000000168cf6940000001f6000000000000000000000000000000000000001a00000000src/shared/CMakeLists.txtadd_library(shared STATIC shared.h shared.cpp types.h types.cpp)
target_link_libraries(shared PUBLIC PkgConfig::glib Qt5::Core Qt5::Widgets Qt5::DBus libappimage translationmanager trashbin)
if(ENABLE_UPDATE_HELPER)
    target_link_libraries(shared PUBLIC libappimageupdate)
endif()
target_compile_definitions(shared
    PRIVATE -DPRIVATE_LIBDIR="${_private_libdir}"
    PRIVATE -DCMAKE_PROJECT_SOURCE_DIR="${PROJECT_SOURCE_DIR}"
)
target_include_directories(shared PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
0707010000002e000081a400000000000000000000000168cf69400000c0cb000000000000000000000000000000000000001600000000src/shared/shared.cpp// system includes
#include <fstream>
#include <iostream>
#include <memory>
#include <sstream>
#include <tuple>
extern "C" {
    #include <appimage/appimage.h>
    #include <glib.h>
    // #include <libgen.h>
    #include <sys/stat.h>
    #include <stdio.h>
    #include <unistd.h>
}

// library includes
#include <QDebug>
#include <QIcon>
#include <QtDBus>
#include <QDirIterator>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QLibraryInfo>
#include <QMap>
#include <QMapIterator>
#include <QMessageBox>
#include <QObject>
#include <QRegularExpression>
#include <QSet>
#include <QSettings>
#include <QStandardPaths>
#include <QWindow>
#include <QPushButton>
#include <QPixmap>
#ifdef ENABLE_UPDATE_HELPER
#include <appimage/update.h>
#endif

// local headers
#include "shared.h"
#include "translationmanager.h"

static void gKeyFileDeleter(GKeyFile* ptr) {
    if (ptr != nullptr)
        g_key_file_free(ptr);
}

static void gErrorDeleter(GError* ptr) {
    if (ptr != nullptr)
        g_error_free(ptr);
}

bool makeExecutable(const QString& path) {
    struct stat fileStat{};

    if (stat(path.toStdString().c_str(), &fileStat) != 0) {
        std::cerr << "Failed to call stat() on " << path.toStdString() << std::endl;
        return false;
    }

    // no action required when file is executable already
    // this could happen in scenarios when an AppImage is in a read-only location
    if ((fileStat.st_uid == getuid() && fileStat.st_mode & 0100) ||
        (fileStat.st_gid == getgid() && fileStat.st_mode & 0010) ||
        (fileStat.st_mode & 0001)) {
        return true;
    }

    return chmod(path.toStdString().c_str(), fileStat.st_mode | 0111) == 0;
}

bool makeNonExecutable(const QString& path) {
    struct stat fileStat{};

    if (stat(path.toStdString().c_str(), &fileStat) != 0) {
        std::cerr << "Failed to call stat() on " << path.toStdString() << std::endl;
        return false;
    }

    auto permissions = fileStat.st_mode;

    // remove executable permissions
    for (const auto permPart : {0100, 0010, 0001}) {
        if (permissions & permPart)
            permissions -= permPart;
    }

    return chmod(path.toStdString().c_str(), permissions) == 0;
}

QString expandTilde(QString path) {
    if ((path.size() == 1 && path[0] == '~') || (path.size() >= 2 && path.startsWith("~/"))) {
        path.remove(0, 1);
        path.prepend(QDir::homePath());
    }

    return path;
}

// calculate path to config file
QString getConfigFilePath() {
    const auto configPath = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation);
    const auto configFilePath = configPath + "/appimagelauncher.cfg";
    return configFilePath;
}

void createConfigFile(int askToMove,
                      const QString& destination,
                      int enableDaemon,
                      const QStringList& additionalDirsToWatch,
                      int monitorMountedFilesystems) {
    auto configFilePath = getConfigFilePath();

    QFile file(configFilePath);
    file.open(QIODevice::WriteOnly);

    // cannot use QSettings because it doesn't support comments
    // let's do it manually and hope for the best
    file.write("[AppImageLauncher]\n");

    if (askToMove < 0) {
        file.write("# ask_to_move = true\n");
    } else {
        file.write("ask_to_move = ");
        if (askToMove == 0) {
            file.write("false");
        } else {
            file.write("true");
        }
        file.write("\n");
    }

    if (destination.isEmpty()) {
        file.write("# destination = ~/Applications\n");
    } else {
        file.write("destination = ");
        file.write(destination.toUtf8());
        file.write("\n");
    }

    if (enableDaemon < 0) {
        file.write("# enable_daemon = true\n");
    } else {
        file.write("enable_daemon = ");
        if (enableDaemon == 0) {
            file.write("false");
        } else {
            file.write("true");
        }
        file.write("\n");
    }

    file.write("\n\n");

    // daemon configs
    file.write("[appimagelauncherd]\n");

    if (additionalDirsToWatch.empty()) {
        file.write("# additional_directories_to_watch = ~/otherApplications:/even/more/applications\n");
    } else {
        file.write("additional_directories_to_watch = ");
        file.write(additionalDirsToWatch.join(':').toUtf8());
        file.write("\n");
    }

    if (monitorMountedFilesystems < 0) {
        file.write("# monitor_mounted_filesystems = false\n");
    } else {
        file.write("monitor_mounted_filesystems = ");
        if (monitorMountedFilesystems == 0) {
            file.write("false");
        } else {
            file.write("true");
        }
        file.write("\n");
    }
}

QSettings* getConfig(QObject* parent) {
    auto configFilePath = getConfigFilePath();

    auto* settings = new QSettings(configFilePath, QSettings::IniFormat, parent);

    // expand ~ in paths in the config file with $HOME
    const auto keysContainingPath = {
        "AppImageLauncher/destination",
    };
    for (const QString& keyContainingPath : keysContainingPath){
        if (settings->contains(keyContainingPath)) {
            auto newValue = expandTilde(settings->value(keyContainingPath).toString());
            settings->setValue(keyContainingPath, newValue);
        }
    }

    return settings;
}

// TODO: check if this works with Wayland
bool isHeadless() {
    bool isHeadless = true;

    // not really clean to abuse env vars as "global storage", but hey, it works
    if (getenv("_FORCE_HEADLESS")) {
        return true;
    }

    QProcess proc;
    proc.setProgram("xhost");
    proc.setStandardOutputFile(QProcess::nullDevice());
    proc.setStandardErrorFile(QProcess::nullDevice());

    proc.start();
    proc.waitForFinished();

    switch (proc.exitCode()) {
        case 255: {
            // program not found, using fallback method
            isHeadless = (getenv("DISPLAY") == nullptr);
            break;
        }
        case 0:
        case 1:
            isHeadless = proc.exitCode() == 1;
            break;
        default:
            throw std::runtime_error("Headless detection failed: unexpected exit code from xhost");
    }

    return isHeadless;
}

// avoids code duplication, and works for both graphical and non-graphical environments
void displayMessageBox(const QString& title, const QString& message, const QMessageBox::Icon icon) {
    if (isHeadless()) {
        std::cerr << title.toStdString() << ": " << message.toStdString() << std::endl;
    } else {
        // little complex, can't use QMessageBox::{critical,warning,...} for the same reason as in main()
        auto* mb = new QMessageBox(icon, title, message, QMessageBox::Ok, nullptr);
        mb->show();
        QApplication::exec();
    }
}

void displayError(const QString& message) {
    displayMessageBox(QObject::tr("Error"), message, QMessageBox::Critical);
}

void displayWarning(const QString& message) {
    displayMessageBox(QObject::tr("Warning"), message, QMessageBox::Warning);
}

QDir integratedAppImagesDestination() {
    auto config = getConfig();

    if (config == nullptr)
        return DEFAULT_INTEGRATION_DESTINATION;

    static const QString keyName("AppImageLauncher/destination");
    if (config->contains(keyName))
        return config->value(keyName).toString();

    return DEFAULT_INTEGRATION_DESTINATION;
}

class Mount {
private:
    QString device;
    QString mountPoint;
    QString fsType;
    QString mountOptions;

public:
    Mount(QString device, QString mountPoint, QString fsType, QString mountOptions) :
        device(std::move(device)),
        mountPoint(std::move(mountPoint)),
        fsType(std::move(fsType)),
        mountOptions(std::move(
        mountOptions)) {}

    Mount(const Mount& other) = default;

    Mount& operator=(const Mount& other) = default;

public:
    const QString& getDevice() const {
        return device;
    }

    const QString& getMountPoint() const {
        return mountPoint;
    }

    const QString& getFsType() const {
        return fsType;
    }

    const QString& getMountOptions() const {
        return mountOptions;
    }
};

QList<Mount> listMounts() {
    QList<Mount> mountedDirectories;

    std::ifstream ifs("/proc/mounts");

    std::string _currentLine;
    while (std::getline(ifs, _currentLine)) {
        const auto currentLine = QString::fromStdString(_currentLine);

        const auto parts = currentLine.split(" ");

        mountedDirectories << Mount{parts[0], parts[1], parts[2], parts[3]};
    }

    return mountedDirectories;
}

QSet<QString> additionalAppImagesLocations(const bool includeAllMountPoints) {
    QSet<QString> additionalLocations;

    additionalLocations << "/Applications";

    // integrate AppImages from mounted filesystems, if requested
    // we don't want to read files from any FUSE mounted filesystems nor from any virtual filesystems
    // to
    static const auto validFilesystems = {"ext2", "ext3", "ext4", "ntfs", "vfat", "btrfs"};

    static const auto blacklistedMountPointPrefixes = {
        "/var/lib/schroot",
        "/run/docker",
        "/boot",
        "/sys",
        "/proc",
        "/snap",
    };

    if (includeAllMountPoints) {
        for (const auto& mount : listMounts()) {
            const auto& device = mount.getDevice();
            const auto& mountPoint = mount.getMountPoint();
            const auto& fsType = mount.getFsType();

            // we have to filter out virtual filesystems, i.e., ones which have a "nonsense" device path
            // any device that doesn't start with / is likely virtual, this is the first indicator
            if (device.size() < 1 || device[0] != '/') {
                continue;
            }

            // the device should exist for obvious reasons
            if (!QFileInfo(QFileInfo(device).absoluteFilePath()).exists()) {
                continue;
            }

            // we don't want to mount any loop-mounted or bind-mounted or other devices, only... "native" ones
            // therefore we permit only "real" devices listed within /dev
            if (!device.startsWith("/dev/")) {
                continue;
            }

            // there's a few locations which we know we don't want to search for AppImages in
            // either it's a waste of time or otherwise a bad idea, but it will surely save time *not* to search them
            if (std::find_if(blacklistedMountPointPrefixes.begin(), blacklistedMountPointPrefixes.end(),
                             [&mountPoint](const QString& prefix) {
                                 return mountPoint == prefix || mountPoint.startsWith(prefix + "/");
                             }) != blacklistedMountPointPrefixes.end()) {
                continue;
            }

            // we can skip the root mount point, as we handled it above
            if (mountPoint == "/") {
                continue;
            }

            // we only support a limited set of filesystems
            if (std::find(validFilesystems.begin(), validFilesystems.end(), fsType) == validFilesystems.end()) {
                continue;
            }

            // sanity check -- can likely be removed in the future
            if (mountPoint.isEmpty()) {
                const auto message = "empty mount point for mount with device " + device.toStdString();
                throw std::invalid_argument(message);
            }

            // assemble potential applications location; caller needs to check whether the directory exists before setting
            // up e.g., an inotify watch
            const QString additionalLocation(mountPoint + "/Applications");
            additionalLocations << additionalLocation;
        }
    }

    return additionalLocations;
}

bool shallMonitorMountedFilesystems(const QSettings* config) {
    Q_ASSERT(config != nullptr);

    return config->value("appimagelauncherd/monitor_mounted_filesystems", "false").toBool();
}

QDirSet getAdditionalDirectoriesFromConfig(const QSettings* config) {
    Q_ASSERT(config != nullptr);

    constexpr auto configKey = "appimagelauncherd/additional_directories_to_watch";
    const auto configValue = config->value(configKey, "").toString();
    qDebug() << configKey << "value:" << configValue;

    QDirSet additionalDirs{};

    for (auto dirPath : configValue.split(":")) {
        // empty values will, for some reason, be interpreted as "use the home directory"
        // as we don't want to accidentally monitor the home directory, we need to skip those values
        if (dirPath.isEmpty()) {
            qDebug() << "skipping empty directory path";
            continue;
        }

        // make sure to have full path
        qDebug() << "path before tilde expansion:" << dirPath;
        dirPath = expandTilde(dirPath);
        qDebug() << "path after tilde expansion:" << dirPath;

        // non-absolute paths which don't contain a tilde cannot be resolved safely, they likley depend on the cwd
        // therefore, we need to ignore those
        if (!QFileInfo(dirPath).isAbsolute()) {
            std::cerr << "Warning: path " << dirPath.toStdString() << " can not be resolved, skipping" << std::endl;
            continue;
        }

        const QDir dir(dirPath);

        if (!dir.exists()) {
            std::cerr << "Warning: could not find directory " << dirPath.toStdString() << ", skipping" << std::endl;
            continue;
        }

        additionalDirs.insert(dir);
    }

    return additionalDirs;
}

QDirSet daemonDirectoriesToWatch(const QSettings* config) {
    QDirSet watchedDirectories;

    // of course we need to watch the main integration directory
    const auto defaultDestination = integratedAppImagesDestination();

    // make sure it exists, otherwise the daemon doesn't have anything to do
    if (!defaultDestination.exists()) {
        defaultDestination.mkdir(".");
    }

    watchedDirectories.insert(defaultDestination);

    // however, there's likely additional ones to watch, like a system-wide Applications directory
    {
        bool monitorMountedFilesystems = shallMonitorMountedFilesystems(config);

        const auto additionalDirs = additionalAppImagesLocations(monitorMountedFilesystems);

        for (const auto& d : additionalDirs) {
            watchedDirectories.insert(QDir(d).absolutePath());
        }
    }

    // also, we should include additional directories from the config file
    {
        const auto configProvidedDirectories = getAdditionalDirectoriesFromConfig(config);

        std::copy(
            configProvidedDirectories.begin(), configProvidedDirectories.end(),
            std::inserter(watchedDirectories, watchedDirectories.end())
        );
    }

    return watchedDirectories;
}

QString buildPathToIntegratedAppImage(const QString& pathToAppImage) {
    // if type 2 AppImage, we can build a "content-aware" filename
    // see #7 for details
    auto digest = getAppImageDigestMd5(pathToAppImage);

    const QFileInfo appImageInfo(pathToAppImage);

    QString baseName = appImageInfo.completeBaseName();

    // if digest is available, append a separator
    if (!digest.isEmpty()) {
        const auto digestSuffix = "_" + digest;

        // check whether digest is already contained in filename
        if (!pathToAppImage.contains(digestSuffix))
            baseName += "_" + digest;
    }

    auto fileName = baseName;

    // must not use completeSuffix() in combination with completeBasename(), otherwise the final filename is composed
    // incorrectly
    if (!appImageInfo.suffix().isEmpty()) {
        fileName += "." + appImageInfo.suffix();
    }

    return integratedAppImagesDestination().path() + "/" + fileName;
}

std::map<std::string, std::string> findCollisions(const QString& currentNameEntry) {
    std::map<std::string, std::string> collisions{};

    // default locations of desktop files on systems
    const auto directories = {
        QString("/usr/share/applications/"),
        QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/applications/"
    };

    for (const auto& directory : directories) {
        QDirIterator iterator(directory, QDirIterator::FollowSymlinks);

        while (iterator.hasNext()) {
            const auto filename = iterator.next();

            if (!QFileInfo(filename).isFile() || !filename.endsWith(".desktop"))
                continue;

            std::shared_ptr<GKeyFile> desktopFile(g_key_file_new(), gKeyFileDeleter);
            std::shared_ptr<GError*> error(nullptr, gErrorDeleter);

            // if the key file parser can't load the file, it's most likely not a valid desktop file, so we just skip this file
            if (!g_key_file_load_from_file(desktopFile.get(), filename.toStdString().c_str(), G_KEY_FILE_KEEP_TRANSLATIONS, error.get()))
                continue;

            auto* nameEntry = g_key_file_get_string(desktopFile.get(), G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_NAME, error.get());

            // invalid desktop file, needs to be skipped
            if (nameEntry == nullptr)
                continue;

            if (QString(nameEntry).trimmed().startsWith(currentNameEntry.trimmed())) {
                collisions[filename.toStdString()] = nameEntry;
            }
        }
    }

    return collisions;
}

bool updateDesktopDatabaseAndIconCaches() {
    const auto dataLocation = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);

    const std::map<std::string, std::string> commands = {
        {"update-desktop-database", dataLocation.toStdString() + "/applications"},
        {"gtk-update-icon-cache-3.0", dataLocation.toStdString() + "/icons/hicolor/ -t"},
        {"gtk-update-icon-cache", dataLocation.toStdString() + "/icons/hicolor/ -t"},
        {"xdg-desktop-menu", "forceupdate"},
        {"update-mime-database", dataLocation.toStdString() + "/mime "},
        {"update-icon-caches", dataLocation.toStdString() + "/icons/"},
    };

    for (const auto& command : commands) {
        // only call if the command exists
        if (system(("which " + command.first + " 2>&1 1>/dev/null").c_str()) == 0) {
            // exit codes are not evaluated intentionally
            system((command.first + " " + command.second).c_str());
        }
    }

    return true;
}

std::shared_ptr<char> getOwnBinaryPath() {
    auto path = std::shared_ptr<char>(realpath("/proc/self/exe", nullptr));

    if (path == nullptr)
        throw std::runtime_error("Could not detect path to own binary; something must be horribly broken");

    return path;
}

#ifndef BUILD_LITE
QString privateLibDirPath(const QString& srcSubdirName) {
    // PRIVATE_LIBDIR will be a relative path most likely
    // therefore, we need to detect the install prefix based on our own binary path, and then calculate the path to
    // the helper tools based on that
    const QString ownBinaryDirPath = QFileInfo(getOwnBinaryPath().get()).dir().absolutePath();
    const QString installPrefixPath = QFileInfo(ownBinaryDirPath).dir().absolutePath();
    QString privateLibDirPath = installPrefixPath + "/" + PRIVATE_LIBDIR;

    // the following lines make things work during development: here, the build dir path is inserted instead, which
    // allows for testing with the latest changes
    if (!QDir(privateLibDirPath).exists()) {
        // this makes sure that when we're running from a local dev build, we end up in the right directory
        // very important when running this code from the daemon, since it's not in the same directory as the helpers
        privateLibDirPath = ownBinaryDirPath + "/../" + srcSubdirName;
    }

    // if there is no such directory like <prefix>/bin/../lib/... or the binary is not found there, there is a chance
    // the binary is just next to this one (this is the case in the update/remove helpers)
    // therefore we compare the binary directory path with PRIVATE_LIBDIR
    if (!QDir(privateLibDirPath).exists()) {
        if (privateLibDirPath.contains(PRIVATE_LIBDIR)) {
            privateLibDirPath = ownBinaryDirPath;
        }
    }

    return privateLibDirPath;
}
#endif

bool installDesktopFileAndIcons(const QString& pathToAppImage, bool resolveCollisions) {
    if (appimage_register_in_system(pathToAppImage.toStdString().c_str(), false) != 0) {
        displayError(QObject::tr("Failed to register AppImage in system via libappimage"));
        return false;
    }

    const auto* desktopFilePath = appimage_registered_desktop_file_path(pathToAppImage.toStdString().c_str(), nullptr, false);

    // sanity check -- if the file doesn't exist, the function returns NULL
    if (desktopFilePath == nullptr) {
        displayError(QObject::tr("Failed to find integrated desktop file"));
        return false;
    }

    // check that file exists
    if (!QFile(desktopFilePath).exists()) {
        displayError(QObject::tr("Couldn't find integrated AppImage's desktop file"));
        return false;
    }

    /* write AppImageLauncher specific entries to desktop file
     *
     * unfortunately, QSettings doesn't work as a desktop file reader/writer, and libqtxdg isn't really meant to be
     * used by projects via add_subdirectory/ExternalProject
     * a system dependency is not an option for this project, and we link to glib already anyway, so let's just use
     * glib, which is known to work
     */

    std::shared_ptr<GKeyFile> desktopFile(g_key_file_new(), gKeyFileDeleter);

    std::shared_ptr<GError*> error(nullptr, gErrorDeleter);

    const auto flags = GKeyFileFlags(G_KEY_FILE_KEEP_COMMENTS | G_KEY_FILE_KEEP_TRANSLATIONS);

    auto handleError = [error, desktopFile]() {
        std::ostringstream ss;
        ss << QObject::tr("Failed to load desktop file:").toStdString() << std::endl << (*error)->message;
        displayError(QString::fromStdString(ss.str()));
    };

    if (!g_key_file_load_from_file(desktopFile.get(), desktopFilePath, flags, error.get())) {
        handleError();
        return false;
    }

    const auto* nameEntry = g_key_file_get_string(desktopFile.get(), G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_NAME, error.get());

    if (nameEntry == nullptr) {
        displayWarning(QObject::tr("AppImage has invalid desktop file"));
    }

    if (resolveCollisions) {
        // TODO: support multilingual collisions
        auto collisions = findCollisions(nameEntry);

        // make sure to remove own entry
        collisions.erase(collisions.find(desktopFilePath));

        if (!collisions.empty()) {
            // collisions are resolved like in the filesystem: a monotonically increasing number in brackets is
            // appended to the Name in order to keep the number monotonically increasing, we look for the highest
            // number in brackets in the existing entries, add 1 to it, and append it in brackets to the current
            // desktop file's Name entry

            unsigned int currentNumber = 1;

            QRegularExpression regex(R"(^.*\(([0-9]+)\)$)");

            for (const auto& collision : collisions) {
                const auto& currentNameEntry = collision.second;

                auto match = regex.match(QString::fromStdString(currentNameEntry));

                if (match.hasMatch()) {
                    // 0 = entire string
                    // 1 = first group
                    const QString numString = match.captured(1);
                    const int num = numString.toInt();

                    // monotonic counting, i.e., never try to "be smart" by e.g., filling in the gaps between
                    // previous numbers
                    if (num >= currentNumber) {
                        currentNumber = num + 1;
                    }
                }
            }

            auto newName = QString(nameEntry) + " (" + QString::number(currentNumber) + ")";
            g_key_file_set_string(desktopFile.get(), G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_NAME, newName.toStdString().c_str());
        }
    }

    auto convertToCharPointerList = [](const std::vector<std::string>& stringList) {
        std::vector<const char*> pointerList;

        // reserve space to increase efficiency
        pointerList.reserve(stringList.size());

        // convert string list to list of const char pointers
        for (const auto& action : stringList) {
            pointerList.push_back(action.c_str());
        }

        return pointerList;
    };

    std::vector<std::string> desktopActions;

    // we may not just overwrite the existing actions key, as then the actions cannot be used any more from the context menu
    {
        const auto* actionsEntry = g_key_file_get_string(desktopFile.get(), G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_ACTIONS, error.get());
        for (const QString& action : QString(actionsEntry).split(";")) {
            if (action.isEmpty()) {
                continue;
            }

            desktopActions.emplace_back(action.toStdString());
        }
    }

    // use a "vendor prefix" to avoid collisions with existing actions, as "Update" and "Remove" are generic terms
    static const std::string removeActionKey{"AppImageLauncher-Remove-AppImage"};
    static const std::string updateActionKey{"AppImageLauncher-Update-AppImage"};

    desktopActions.emplace_back(removeActionKey);

    // load translations from JSON file(s)
    QMap<QString, QString> removeActionNameTranslations;

#ifdef ENABLE_UPDATE_HELPER
    QMap<QString, QString> updateActionNameTranslations;

    {
        QDirIterator i18nDirIterator(TranslationManager::getTranslationDir());

        while(i18nDirIterator.hasNext()) {
            const auto& filePath = i18nDirIterator.next();
            const auto& fileName = QFileInfo(filePath).fileName();

            if (!QFileInfo(filePath).isFile() || !(fileName.startsWith("desktopfiles.") && fileName.endsWith(".json")))
                continue;

            // check whether filename's format is alright, otherwise parsing the locale might try to access a
            // non-existing (or the wrong) member
            auto splitFilename = fileName.split(".");

            if (splitFilename.size() != 3)
                continue;

            // parse locale from filename
            auto locale = splitFilename[1];

            QFile jsonFile(filePath);

            if (!jsonFile.open(QIODevice::ReadOnly)) {
                displayWarning(QMessageBox::tr("Could not parse desktop file translations:\nCould not open file for reading:\n\n%1").arg(fileName));
            }

            // TODO: need to make sure that this doesn't try to read huge files at once
            auto data = jsonFile.readAll();

            QJsonParseError parseError{};
            auto jsonDoc = QJsonDocument::fromJson(data, &parseError);

            // show warning on syntax errors and continue
            if (parseError.error != QJsonParseError::NoError || jsonDoc.isNull() || !jsonDoc.isObject()) {
                displayWarning(QMessageBox::tr("Could not parse desktop file translations:\nInvalid syntax:\n\n%1").arg(parseError.errorString()));
            }

            auto jsonObj = jsonDoc.object();

            for (const auto& key : jsonObj.keys()) {
                auto value = jsonObj[key].toString();
                auto splitKey = key.split("/");

                if (key.startsWith("Desktop Action update")) {
                    qDebug() << "update: adding" << value << "for locale" << locale;
                    updateActionNameTranslations[locale] = value;
                } else if (key.startsWith("Desktop Action remove")) {
                    qDebug() << "remove: adding" << value << "for locale" << locale;
                    removeActionNameTranslations[locale] = value;
                }
            }
        }
    }
#endif

#ifndef BUILD_LITE
    auto privateLibDir = privateLibDirPath("ui");

    const char helperIconName[] = "AppImageLauncher";
#else
    const char helperIconName[] = "AppImageLauncher-Lite";
#endif

    // add Remove action
    {
        const auto removeSectionName = "Desktop Action " + removeActionKey;

        g_key_file_set_string(desktopFile.get(), removeSectionName.c_str(), "Name", "Delete this AppImage");
        g_key_file_set_string(desktopFile.get(), removeSectionName.c_str(), "Icon", helperIconName);

        std::ostringstream removeExecPath;

#ifndef BUILD_LITE
        removeExecPath << privateLibDir.toStdString() << "/remove";
#else
        removeExecPath << getenv("HOME") << "/.local/lib/appimagelauncher-lite/appimagelauncher-lite.AppImage remove";
#endif

        removeExecPath << " \"" << pathToAppImage.toStdString() << "\"";

        g_key_file_set_string(desktopFile.get(), removeSectionName.c_str(), "Exec", removeExecPath.str().c_str());

        // install translations
        auto it = QMapIterator<QString, QString>(removeActionNameTranslations);
        while (it.hasNext()) {
            auto entry = it.next();
            g_key_file_set_locale_string(desktopFile.get(), removeSectionName.c_str(), "Name", entry.key().toStdString().c_str(), entry.value().toStdString().c_str());
        }
    }

#ifdef ENABLE_UPDATE_HELPER
    // add Update action
    {
        appimage::update::Updater updater(pathToAppImage.toStdString());

        // but only if there's update information
        if (!updater.updateInformation().empty()) {
            // section needs to be announced in desktop actions list
            desktopActions.emplace_back(updateActionKey);

            const auto updateSectionName = "Desktop Action " + updateActionKey;

            g_key_file_set_string(desktopFile.get(), updateSectionName.c_str(), "Name", "Update this AppImage");
            g_key_file_set_string(desktopFile.get(), updateSectionName.c_str(), "Icon", helperIconName);

            std::ostringstream updateExecPath;

#ifndef BUILD_LITE
            updateExecPath << privateLibDir.toStdString() << "/update";
#else
            updateExecPath << getenv("HOME") << "/.local/lib/appimagelauncher-lite/appimagelauncher-lite.AppImage update";
#endif
            updateExecPath << " \"" << pathToAppImage.toStdString() << "\"";

            g_key_file_set_string(desktopFile.get(), updateSectionName.c_str(), "Exec", updateExecPath.str().c_str());

            // install translations
            auto it = QMapIterator<QString, QString>(updateActionNameTranslations);
            while (it.hasNext()) {
                auto entry = it.next();
                g_key_file_set_locale_string(desktopFile.get(), updateSectionName.c_str(), "Name", entry.key().toStdString().c_str(), entry.value().toStdString().c_str());
            }
        }
    }
#endif

    // add desktop actions key
    g_key_file_set_string_list(
            desktopFile.get(),
            G_KEY_FILE_DESKTOP_GROUP,
            G_KEY_FILE_DESKTOP_KEY_ACTIONS,
            convertToCharPointerList(desktopActions).data(),
            desktopActions.size()
    );

    // add version key
    const auto version = QApplication::applicationVersion().replace("version ", "").toStdString();
    g_key_file_set_string(desktopFile.get(), G_KEY_FILE_DESKTOP_GROUP, "X-AppImageLauncher-Version", version.c_str());

    // save desktop file to disk
    if (!g_key_file_save_to_file(desktopFile.get(), desktopFilePath, error.get())) {
        handleError();
        return false;
    }

    // make desktop file executable ("trustworthy" to some DEs)
    // TODO: handle this in libappimage
    makeExecutable(desktopFilePath);

    // notify KDE/Plasma about icon change
    {
        auto message = QDBusMessage::createSignal(QStringLiteral("/KIconLoader"), QStringLiteral("org.kde.KIconLoader"), QStringLiteral("iconChanged"));
        message.setArguments({0});
        QDBusConnection::sessionBus().send(message);
    }

    return true;
}

bool updateDesktopFileAndIcons(const QString& pathToAppImage) {
    return installDesktopFileAndIcons(pathToAppImage, true);
}

IntegrationState integrateAppImage(const QString& pathToAppImage, const QString& pathToIntegratedAppImage) {
    // need std::strings to get working pointers with .c_str()
    const auto oldPath = pathToAppImage.toStdString();
    const auto newPath = pathToIntegratedAppImage.toStdString();

    // create target directory
    QDir().mkdir(QFileInfo(QFile(pathToIntegratedAppImage)).dir().absolutePath());

    // check whether AppImage is in integration directory already
    if (QFileInfo(pathToAppImage).absoluteFilePath() != QFileInfo(pathToIntegratedAppImage).absoluteFilePath()) {
        // need to check whether file exists
        // if it does, the existing AppImage needs to be removed before rename can be called
        if (QFile(pathToIntegratedAppImage).exists()) {
            std::ostringstream message;
            message << QObject::tr("AppImage with same filename has already been integrated.").toStdString() << std::endl
                    << std::endl
                    << QObject::tr("Do you wish to overwrite the existing AppImage?").toStdString() << std::endl
                    << QObject::tr("Choosing No will run the AppImage once, and leave the system in its current state.").toStdString();

            auto* messageBox = new QMessageBox(
                QMessageBox::Warning,
                QObject::tr("Warning"),
                QString::fromStdString(message.str()),
                QMessageBox::Yes | QMessageBox::No
            );

            messageBox->setDefaultButton(QMessageBox::No);
            messageBox->show();

            QApplication::exec();

            if (messageBox->clickedButton() == messageBox->button(QMessageBox::No)) {
                return INTEGRATION_ABORTED;
            }

            QFile(pathToIntegratedAppImage).remove();
        }

        if (!QFile(pathToAppImage).rename(pathToIntegratedAppImage)) {
            auto* messageBox = new QMessageBox(
                QMessageBox::Critical,
                QObject::tr("Error"),
                QObject::tr("Failed to move AppImage to target location.\n"
                            "Try to copy AppImage instead?"),
                QMessageBox::Ok | QMessageBox::Cancel
            );

            messageBox->setDefaultButton(QMessageBox::Ok);
            messageBox->show();

            QApplication::exec();

            if (messageBox->clickedButton() == messageBox->button(QMessageBox::Cancel))
                return INTEGRATION_FAILED;

            if (!QFile(pathToAppImage).copy(pathToIntegratedAppImage)) {
                displayError("Failed to copy AppImage to target location");
                return INTEGRATION_FAILED;
            }
        }
    }

    if (!installDesktopFileAndIcons(pathToIntegratedAppImage))
        return INTEGRATION_FAILED;

    return INTEGRATION_SUCCESSFUL;
}

QString getAppImageDigestMd5(const QString& path) {
    // try to read embedded MD5 digest
    unsigned long offset = 0, length = 0;

    // first of all, digest calculation is supported only for type 2
    if (appimage_get_type(path.toStdString().c_str(), false) != 2)
        return "";

    auto rv = appimage_get_elf_section_offset_and_length(path.toStdString().c_str(), ".digest_md5", &offset, &length);

    QByteArray buffer(16, '\0');

    if (rv && offset != 0 && length != 0) {
        // open file and read digest from ELF header section
        QFile file(path);

        if (!file.open(QFile::ReadOnly))
            return "";

        if (!file.seek(static_cast<qint64>(offset)))
            return "";

        if (!file.read(buffer.data(), buffer.size()))
            return "";

        file.close();
    }

    bool needToCalculateDigest;

    // there seem to be some AppImages out there who actually have the required section embedded, but it's empty
    // therefore we make the assumption that a hash value of zeroes is probably incorrect and recalculate
    // in the extremely rare case in which the AppImage's digest would *really* be that value, we'd waste a bit of
    // computation time, but the chances are so low... who cares, right?
    {
        auto nonZeroCharacterFound = false;

        for (const char i : buffer) {
            if (i != '\0') {
                nonZeroCharacterFound = true;
                break;
            }
        }

        needToCalculateDigest = !nonZeroCharacterFound;
    }

    if (needToCalculateDigest) {
        // calculate digest
        if (!appimage_type2_digest_md5(path.toStdString().c_str(), buffer.data()))
            return "";
    }

    // create hexadecimal representation
    auto hexDigest = appimage_hexlify(buffer, static_cast<size_t>(buffer.size()));

    QString hexDigestStr(hexDigest);

    free(hexDigest);

    return hexDigestStr;
}

bool hasAlreadyBeenIntegrated(const QString& pathToAppImage) {
    return appimage_is_registered_in_system(pathToAppImage.toStdString().c_str());
}

bool isInDirectory(const QString& pathToAppImage, const QDir& directory) {
    return directory == QFileInfo(pathToAppImage).absoluteDir();
}

bool cleanUpOldDesktopIntegrationResources(bool verbose) {
    auto dirPath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/applications";

    auto directory = QDir(dirPath);

    QStringList filters;
    filters << "appimagekit_*.desktop";

    directory.setNameFilters(filters);

    for (auto desktopFilePath : directory.entryList()) {
        desktopFilePath = dirPath + "/" + desktopFilePath;

        std::shared_ptr<GKeyFile> desktopFile(g_key_file_new(), [](GKeyFile* p) {
            g_key_file_free(p);
        });

        if (!g_key_file_load_from_file(desktopFile.get(), desktopFilePath.toStdString().c_str(), G_KEY_FILE_NONE, nullptr)) {
            continue;
        }

        std::shared_ptr<char> execValue(g_key_file_get_string(desktopFile.get(), G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_EXEC, nullptr), [](char* p) {
            free(p);
        });

        // if there is no Exec value in the file, the desktop file is apparently broken, therefore we skip the file
        if (execValue == nullptr) {
            continue;
        }

        std::shared_ptr<char> tryExecValue(g_key_file_get_string(desktopFile.get(), G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_TRY_EXEC, nullptr), [](char* p) {
            free(p);
        });

        // TryExec is optional, although recently the desktop integration functions started to force add such keys
        // with a path to the desktop file
        // (before, if it existed, the key was replaced with the AppImage's path)
        // If it exists, we assume its value is the full path to the AppImage, which can be used to check the existence
        // of the AppImage
        QString appImagePath;

        if (tryExecValue != nullptr) {
            appImagePath = QString(tryExecValue.get());
        } else {
            appImagePath = QString(execValue.get()).split(" ").first();
        }

        // now, check whether AppImage exists
        // FIXME: the split command for the Exec value might not work if there's a space in the filename
        // we really need a parser that understands the desktop file escaping
        if (!QFile(appImagePath).exists()) {
            if (verbose)
                std::cout << "AppImage no longer exists, cleaning up resources: " << appImagePath.toStdString() << std::endl;

            if (verbose)
                std::cout << "Removing desktop file: " << desktopFilePath.toStdString() << std::endl;

            QFile(desktopFilePath).remove();

            // TODO: clean up related resources such as icons or MIME definitions

            auto* iconValue = g_key_file_get_string(desktopFile.get(), G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_ICON, nullptr);

            if (iconValue != nullptr) {
                const auto dataLocation = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
                const auto iconsPath = QString::fromStdString(dataLocation.toStdString() + "/share/icons/");

                for (QDirIterator it(iconsPath, QDirIterator::Subdirectories); it.hasNext();) {
                    auto path = it.next();

                    if (QFileInfo(path).completeBaseName().startsWith(iconValue)) {
                        QFile::remove(path);
                    }
                }
            }
        }
    }

    return true;
}

time_t getMTime(const QString& path) {
    struct stat st{};
    if (stat(path.toStdString().c_str(), &st) != 0) {
        displayError(QObject::tr("Failed to call stat() on path:\n\n%1").arg(path));
        return -1;
    }

    return st.st_mtim.tv_sec;
}

bool desktopFileHasBeenUpdatedSinceLastUpdate(const QString& pathToAppImage) {
    const auto ownBinaryPath = getOwnBinaryPath();

    const auto desktopFilePath = appimage_registered_desktop_file_path(pathToAppImage.toStdString().c_str(), nullptr, false);
    
    auto ownBinaryMTime = getMTime(ownBinaryPath.get());
    auto desktopFileMTime = getMTime(desktopFilePath);

    // check if something has failed horribly
    if (desktopFileMTime < 0 || ownBinaryMTime < 0)
        return false;

    return desktopFileMTime > ownBinaryMTime;
}

bool isAppImage(const QString& path) {
    const auto type = appimage_get_type(path.toUtf8(), false);
    return type > 0 && type <= 2;
}

QString which(const std::string& name) {
    std::vector<char> command(4096);
    snprintf(command.data(), command.size()-1, "which %s", name.c_str());

    auto* proc = popen(command.data(), "r");

    if (proc == nullptr)
        throw std::runtime_error("Failed to start process for which");

    std::vector<char> outBuf(4096);

    fread(outBuf.data(), sizeof(char), outBuf.size()-1, proc);

    pclose(proc);

    QString rv(outBuf.data());

    rv.replace("\n", "");

    return rv;
}

void checkAuthorizationAndShowDialogIfNecessary(const QString& path, const QString& question) {
    const uint32_t ownUid = getuid();
    const uint32_t fileOwnerUid = QFileInfo(path).ownerId();
    const auto fileOwnerUsername = QFileInfo(path).owner();

    if (ownUid != fileOwnerUid) {
        qDebug() << "attempting relaunch with root helper";

        QString messageBoxText = QMessageBox::tr("File %1 is owned by another user: %2").arg(path).arg(fileOwnerUsername);
        messageBoxText += "\n\n";
        messageBoxText += question;

        auto* messageBox = new QMessageBox(
            QMessageBox::Warning,
            QMessageBox::tr("Permissions problem"),
            messageBoxText,
            QMessageBox::Ok | QMessageBox::Abort,
            nullptr
        );

        messageBox->setDefaultButton(QMessageBox::Ok);
        messageBox->show();

        QApplication::exec();

        const auto relaunch = messageBox->clickedButton() == messageBox->button(QMessageBox::Ok);

        if (!relaunch) {
            qDebug() << "Dialog aborted";
            exit(1);
        }

        qDebug() << "ok, attempting relaunch with root helper";

        // pkexec doesn't retain $DISPLAY etc., as per the man page, so we can't run UI programs with it
        for (const auto& rootHelperFilename : {/*"pkexec",*/ "gksudo", "gksu"}) {
            const auto rootHelperPath = which(rootHelperFilename);
            qDebug() << "trying root helper " << rootHelperFilename << rootHelperPath;

            if (rootHelperPath.isEmpty())
                continue;

            qDebug() << rootHelperFilename << rootHelperPath;

            std::vector<char*> argv = {
                strdup(rootHelperPath.toStdString().c_str()),
            };

            if (fileOwnerUid != 0) {
                argv.emplace_back(strdup("--user"));
                argv.emplace_back(strdup(std::to_string(fileOwnerUid).c_str()));
            }

            for (const auto& arg : QCoreApplication::arguments()) {
                argv.emplace_back(strdup(arg.toStdString().c_str()));
            }

            argv.emplace_back(nullptr);

            const auto rv = execv(strdup(rootHelperPath.toStdString().c_str()), argv.data());

            // if the execution fails, we should signalize this to the user instead of silently failing over to the
            // next tool
            QMessageBox::critical(
                    nullptr,
                    QMessageBox::tr("Error"),
                    QMessageBox::tr("Failed to run permissions helper, exited with return code %1").arg(rv)
            );
            exit(1);
        }

        QMessageBox::critical(
            nullptr,
            QMessageBox::tr("Error"),
            QMessageBox::tr("Could not find suitable permissions helper, aborting")
        );
        exit(1);
    }
}

QString pathToPrivateDataDirectory() {
    // first we need to find the translation directory
    // if this is run from the build tree, we try a path that can only work within the build directory
    // then, we try the expected install location relative to the main binary
    const auto binaryDirPath = QApplication::applicationDirPath();

    // our helper tools are not shipped in usr/bin but usr/lib/<arch>-linux-gnu/appimagelauncher
    // therefore we need to check for the translations directory relative to this directory as well
    // as <arch-linux-gnu> may not be used in the path, we also check for its parent directory
    QString dataDir = binaryDirPath + "/../../share/appimagelauncher/";

    if (!QDir(dataDir).exists()) {
        dataDir = binaryDirPath + "/../../../share/appimagelauncher/";
    }

    // this directory should work for the main application in usr/bin
    if (!QDir(dataDir).exists()) {
        dataDir = binaryDirPath + "/../share/appimagelauncher/";
    }

    if (!QDir(dataDir).exists()) {
        std::cerr << "[AppImageLauncher] Warning: "
                  << "Path to private data directory could not be found" << std::endl;
        return "";
    }

    return dataDir;
}

bool unregisterAppImage(const QString& pathToAppImage) {
    auto rv = appimage_unregister_in_system(pathToAppImage.toStdString().c_str(), false);

    if (rv != 0)
        return false;

    return true;
}

QIcon loadIconWithFallback(const QString& iconName) {
    const QString subdirName("fallback-icons");
    const auto binaryDir = QApplication::applicationDirPath();

    // first we check the directory that would be expected with in the build environment
    QDir fallbackIconDirectory = QDir(binaryDir + "/../../resources/" + subdirName);

    // if that doesn't work, we check the private data directory, which should work when AppImageLauncher is installed
    // through the packages or in Lite's AppImage
    if (!fallbackIconDirectory.exists()) {
        auto privateDataDir = pathToPrivateDataDirectory();

        if (privateDataDir.length() > 0 && QDir(privateDataDir).exists()) {
            fallbackIconDirectory = QDir(pathToPrivateDataDirectory() + "/" + subdirName);
        }
    }

    // fallback icons aren't critical enough to exit the application if they can't be found
    // after all, the theme icons may work just as well
    if (!fallbackIconDirectory.exists()) {
        std::cerr << "[AppImageLauncher] Warning:"
                  << "fallback icons could not be loaded: directory could not be found" << std::endl;
        return QIcon{};
    }

    qDebug() << "Loading fallback for icon" << iconName;

    const auto iconFilename = iconName + ".svg";
    const auto iconPath = fallbackIconDirectory.filePath(iconFilename);

    if (!QFileInfo(iconPath).isFile()) {
        std::cerr << "[AppImageLauncher] Warning: can't find fallback icon for name"
                  << iconName.toStdString() << std::endl;
        return QIcon{};
    }

    const auto fallbackIcon = QIcon(iconPath);
    qDebug() << fallbackIcon;

    return fallbackIcon;
}

void setUpFallbackIconPaths(QWidget* parent) {
    /**
     * Qt 5.12 adds a feature to add fallback paths for icons. This is a very simple way to automatically load custom
     * icons when the icon theme doesn't provide a suitable alternative.
     * However, we need to support a much older Qt version. Therefore we cannot use this very very handy feature.
     * We basically iterate over all buttons which carry an icon and (re)load it, but this time provide a fallback
     * loaded from our private data directory.
     */

    // for now we only support buttons
    // we could always add more widgets which provide an icon property
    const auto buttons = parent->findChildren<QAbstractButton*>();

    for (const auto& button : buttons) {
        const auto iconName = button->icon().name();

        // sort out buttons without an icon
        if (iconName.length() <= 0)
            continue;

        // load icon from theme, providing the bundled icon as a fallback
        // loading an "empty" (i.e., isNull() returns true) icon as fallback, as returned by loadIconWithFallback(...),
        // works just fine
        auto fallbackIcon = loadIconWithFallback(iconName);
        auto newIcon = QIcon::fromTheme(iconName, fallbackIcon);

        if (newIcon.isNull() || newIcon.pixmap(16, 16).isNull())
            newIcon = fallbackIcon;

        // now replace the button's actual icon with the fallback-enabled one
        button->setIcon(newIcon);
    }
}
0707010000002f000081a400000000000000000000000168cf69400000162f000000000000000000000000000000000000001400000000src/shared/shared.h/* central file for utility functions */

#pragma once

// system headers
#include <string>
#include <memory>

// library headers
#include <QDir>
#include <QString>
#include <QSettings>

// local headers
#include "types.h"

enum IntegrationState {
    INTEGRATION_FAILED = 0,
    INTEGRATION_SUCCESSFUL,
    INTEGRATION_ABORTED
};

// standard location for integrated AppImages
// currently hardcoded, can not be changed by users
static const auto DEFAULT_INTEGRATION_DESTINATION = QString(getenv("HOME")) + "/Applications/";

// little convenience method to display warnings
void displayWarning(const QString& message);

// little convenience method to display errors
void displayError(const QString& message);

// reliable way to check if the current session is graphical or not
bool isHeadless();

// makes an existing file executable
bool makeExecutable(const QString& path);

// removes executable bits from file's permissions
bool makeNonExecutable(const QString& path);

#ifndef BUILD_LITE
// calculate path to private libdir, containing tools and libraries specific to and used by AppImageLauncher
QString privateLibDirPath(const QString& srcSubdirName);
#endif

// installs desktop file for given AppImage, including AppImageLauncher specific modifications
// set resolveCollisions to false in order to leave the Name entries as-is
bool installDesktopFileAndIcons(const QString& pathToAppImage, bool resolveCollisions = true);

// update AppImage's existing desktop file with AppImageLauncher specific entries
// this alias for installDesktopFileAndIcons does not perform any collision detection and resolving
bool updateDesktopFileAndIcons(const QString& pathToAppImage);

// update desktop database and icon caches of desktop environments
// this makes sure that:
//   - outdated entries are removed from the launcher
//   - icons of freshly integrated AppImages are displayed in the launcher
bool updateDesktopDatabaseAndIconCaches();

// integrates an AppImage using a standard workflow used across all AppImageLauncher applications
IntegrationState integrateAppImage(const QString& pathToAppImage, const QString& pathToIntegratedAppImage);

// write config file to standard location with given configuration values
// askToMove and enableDaemon both are bools but represented as int to add some sort of "unset" state
// < 0: unset; 0 = false; > 0 = true
// destination is a string that, when empty, will be interpreted as "use default"
void createConfigFile(int askToMove, const QString& destination, int enableDaemon,
                      const QStringList& additionalDirsToWatch = {}, int monitorMountedFilesystems = -1);

// replaces ~ character in paths with real home directory, if necessary and possible
QString expandTilde(QString path);

// load config file and return it
QSettings* getConfig(QObject* parent = nullptr);

// return directory into which the integrated AppImages will be moved
QDir integratedAppImagesDestination();

// additional directories to monitor for AppImages, and to permit AppImages to be within (i.e., shall not ask whether
// to move to the main location, if they're in one of these, it's all good)
QSet<QString> additionalAppImagesLocations(bool includeValidMountPoints = false);

// calculate list of directories the daemon has to watch
// AppImages inside there should furthermore not be moved out of there and into the main integration directory
QDirSet daemonDirectoriesToWatch(const QSettings* config);

// build path to standard location for integrated AppImages
QString buildPathToIntegratedAppImage(const QString& pathToAppImage);

// get AppImage MD5 digest
// extracts the digest embedded in the file
// if no such digest has been embedded, it calculates it using libappimage
QString getAppImageDigestMd5(const QString& path);

// checks whether AppImage has been integrated already
bool hasAlreadyBeenIntegrated(const QString& pathToAppImage);

// checks whether file is in a given directory
bool isInDirectory(const QString& pathToAppImage, const QDir& directory);

// clean up old desktop files (and related resources, such as icons)
bool cleanUpOldDesktopIntegrationResources(bool verbose = false);

// returns absolute path to currently running binary
std::shared_ptr<char> getOwnBinaryPath();

// returns true if AppImageLauncher was updated since the desktop file for a given AppImage has been updated last
bool desktopFileHasBeenUpdatedSinceLastUpdate(const QString& pathToAppImage);

// checks whether a file is an AppImage
bool isAppImage(const QString& path);

// when a file doesn't belong to the current user, this method shows a dialog asking whether to relaunch as that user
// this can be used when e.g., updating AppImages owned by root or other users
// uses pkexec, gksudo, gksu etc., whatever is available
// the second argument is the question that will be asked in the dialog displayed in case a relaunch is necessary
void checkAuthorizationAndShowDialogIfNecessary(const QString& path, const QString& question);

// searchs for path to private data directory relative to the current binary's location
// returns empty string if the path cannot be found
QString pathToPrivateDataDirectory();

// clean up desktop integration files installed while originally integrating the AppImage
bool unregisterAppImage(const QString& pathToAppImage);

// try to load icon with provided name from AppImageLauncher's fallback icons directory
// returns empty QIcon if such an icon cannot be found
// you can check for errors by calling QIcon::isNull()
QIcon loadIconWithFallback(const QString& iconName);

// sets up paths to fallback icons bundled with AppImageLauncher
void setUpFallbackIconPaths(QWidget*);
07070100000030000081a400000000000000000000000168cf6940000000c1000000000000000000000000000000000000001500000000src/shared/types.cpp#include "types.h"

QDebug operator<<(QDebug debug, const QDirSet& set) {
    QDebugStateSaver saver(debug);
    for (const auto& item : set) {
        debug << item;
    }
    return debug;
}
07070100000031000081a400000000000000000000000168cf694000000167000000000000000000000000000000000000001300000000src/shared/types.h#pragma once

// system headers
#include <set>

// library headers
#include <QDebug>
#include <QDir>

struct QDirComparator {
public:
    size_t operator()(const QDir& a, const QDir& b) const {
        return a.absolutePath() < b.absolutePath();
    }
};

typedef std::set<QDir, QDirComparator> QDirSet;


QDebug operator<<(QDebug debug, const QDirSet &set);
07070100000032000041ed00000000000000000000000168cf694000000000000000000000000000000000000000000000000b00000000src/shared07070100000033000081a400000000000000000000000168cf6940000000cb000000000000000000000000000000000000001c00000000src/trashbin/CMakeLists.txtadd_library(trashbin STATIC trashbin.cpp trashbin.h)
target_link_libraries(trashbin PUBLIC Qt5::Core libappimage shared)
target_include_directories(translationmanager PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
07070100000034000081a400000000000000000000000168cf694000000a91000000000000000000000000000000000000001a00000000src/trashbin/trashbin.cpp// system includes
#include <iostream>
#include <sys/stat.h>

// library includes
#include <QDateTime>
#include <QDir>
#include <QDirIterator>
#include <appimage/appimage.h>

// local includes
#include "trashbin.h"
#include "shared.h"

class TrashBin::PrivateData {
    public:
        const QDir dir;

    public:
        PrivateData() : dir(integratedAppImagesDestination().path() + "/.trash") {
            // make sure trash directory exists
            QDir(integratedAppImagesDestination().path()).mkdir(".trash");
        }

        bool canBeCleanedUp(const QString& path) {
            return true;
        }
};

TrashBin::TrashBin() {
    d = new PrivateData();
}

QString TrashBin::path() {
    return d->dir.path();
}

bool TrashBin::disposeAppImage(const QString& pathToAppImage) {
    if (!QFile(pathToAppImage).exists()) {
        std::cerr << "No such file or directory: " << pathToAppImage.toStdString() << std::endl;
        return false;
    }

    // moving AppImages into the trash bin might fail if there's a file with the same filename
    // removing multiple files with the same filenames is a valid use case, though
    // therefore, a timestamp shall be prepended to the filename
    // it is very unlike that some user will remove more than a AppImage per second, but if that should be the case,
    // we could eventually increase the precision of the timestamp
    // for now, this is not necessary
    auto timestamp = QDateTime::currentDateTime().toString(Qt::ISODate);
    auto newPath = d->dir.path() + QString("/") + timestamp + "_" + QFileInfo(pathToAppImage).fileName();

    if (!QFile(pathToAppImage).rename(newPath))
        return false;

    if (!makeNonExecutable(newPath))
        return false;

    return true;
}

bool TrashBin::cleanUp() {
    for (QDirIterator iterator(d->dir, QDirIterator::FollowSymlinks); iterator.hasNext();) {
        auto currentPath = iterator.next();

        if (!QFileInfo(currentPath).isFile())
            continue;

        if (appimage_get_type(currentPath.toStdString().c_str(), false) <= 0)
            continue;

        if (!d->canBeCleanedUp(currentPath)) {
            std::cerr << "Cannot clean up AppImage yet: " << currentPath.toStdString() << std::endl;
            continue;
        }

        std::cerr << "Removing AppImage: " << currentPath.toStdString() << std::endl;

        // silently ignore if files can not be removed
        // they shall be removed on subsequent runs
        // if this won't happen and the trash directory will only get bigger at some point, we might need to
        // reconsider this decision
        if (!QFile(currentPath).remove())
            continue;
    }

    return true;
}
07070100000035000081a400000000000000000000000168cf694000000236000000000000000000000000000000000000001800000000src/trashbin/trashbin.h// system includes
#include <QString>

#pragma once

class TrashBin {
    private:
        class PrivateData;
        PrivateData* d;

    public:
        TrashBin();

    public:
        QString path();

    public:
        // move AppImage into trash bin directory
        bool disposeAppImage(const QString& pathToAppImage);

        // check all AppImages in trash bin whether they can be removed
        // this function should be called regularly to make sure the files in the trash bin are cleaned up as soon
        // as possible
        bool cleanUp();
};
07070100000036000041ed00000000000000000000000168cf694000000000000000000000000000000000000000000000000d00000000src/trashbin07070100000037000081a400000000000000000000000168cf6940000009a7000000000000000000000000000000000000001600000000src/ui/CMakeLists.txtif(NOT BUILD_LITE)
    # main AppImageLauncher application
    add_executable(AppImageLauncher main.cpp resources.qrc first-run.cpp first-run.h first-run.ui integration_dialog.cpp integration_dialog.h integration_dialog.ui)
    target_link_libraries(AppImageLauncher shared PkgConfig::glib libappimage shared)

    # set binary runtime rpath to make sure the libappimage.so built and installed by this project is going to be used
    # by the installed binaries (be it the .deb, the AppImage, or whatever)
    # in order to make the whole install tree relocatable, a relative path is used
    set_target_properties(AppImageLauncher PROPERTIES INSTALL_RPATH ${_rpath})

    install(
        TARGETS
        AppImageLauncher
        RUNTIME DESTINATION ${_bindir} COMPONENT APPIMAGELAUNCHER
        LIBRARY DESTINATION ${_libdir} COMPONENT APPIMAGELAUNCHER
    )
endif()

# AppImageLauncherSettings application
add_executable(AppImageLauncherSettings settings_main.cpp resources.qrc settings_dialog.ui settings_dialog.cpp)
target_link_libraries(AppImageLauncherSettings shared)

# set binary runtime rpath to make sure the libappimage.so built and installed by this project is going to be used
# by the installed binaries (be it the .deb, the AppImage, or whatever)
# in order to make the whole install tree relocatable, a relative path is used
set_target_properties(AppImageLauncherSettings PROPERTIES INSTALL_RPATH ${_rpath})

install(
    TARGETS
    AppImageLauncherSettings
    RUNTIME DESTINATION ${_bindir} COMPONENT APPIMAGELAUNCHER
    LIBRARY DESTINATION ${_libdir} COMPONENT APPIMAGELAUNCHER
)

# AppImage removal helper
add_executable(remove remove_main.cpp remove.ui resources.qrc)
target_link_libraries(remove shared translationmanager libappimage)
# see AppImageLauncher for a description
set_target_properties(remove PROPERTIES INSTALL_RPATH "\$ORIGIN")

install(
    TARGETS
    remove
    RUNTIME DESTINATION ${_private_libdir} COMPONENT APPIMAGELAUNCHER
)


# AppImage update helper
if(ENABLE_UPDATE_HELPER)
    add_executable(update update.ui update_main.cpp resources.qrc)
    target_link_libraries(update shared translationmanager libappimage libappimageupdate-qt Qt5::Quick Qt5::QuickWidgets Qt5::Qml)
    # see AppImageLauncher for a description
    set_target_properties(update PROPERTIES INSTALL_RPATH "\$ORIGIN")

    install(
        TARGETS
        update
        RUNTIME DESTINATION ${_private_libdir} COMPONENT APPIMAGELAUNCHER
    )
endif()
07070100000038000081a400000000000000000000000168cf694000001467000000000000000000000000000000000000001500000000src/ui/first-run.cpp// system includes
#include <stdexcept>

// library includes
#include <QDesktopServices>
#include <QDialog>
#include <QDialogButtonBox>
#include <QLabel>
#include <QDebug>
#include <QFileDialog>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsPixmapItem>
#include <QImage>
#include <QLayout>
#include <QStyle>
#include <QUrl>

// local includes
#include "ui_first-run.h"
#include "shared.h"


class FirstRunDialog : public QDialog {
private:
    Ui::FirstRunDialog* firstRunDialog{};

    // stores custom destination dir for AppImages
    // if this is empty, the default value (defined in-code, might change over time) will be chosen
    // the default will not be written to the config file, hence we need a way to detect that state, and it's assumed
    // that when this string is empty, that's the case
    // we could also just compare this directory with the default value just before saving, but IMO it's more obvious
    // to the user that the "default" state is lost after having saved something with the "choose" button in a file
    // dialog
    QString destinationDir;

private Q_SLOTS:
    void resetDefaults() {
        firstRunDialog->askMoveCheckBox->setChecked(true);

        destinationDir = "";
        updateDestinationDirLabel();
    }

    void handleButtonClicked(QAbstractButton* button) {
        if (button == firstRunDialog->buttonBox->button(QDialogButtonBox::RestoreDefaults)) {
            qDebug() << "restore defaults";
            resetDefaults();
        } else if (button == firstRunDialog->buttonBox->button(QDialogButtonBox::Help)) {
            qDebug() << "help";
            QDesktopServices::openUrl(QUrl("https://github.com/TheAssassin/AppImageLauncher/wiki/First-run"));

        } else {
            qDebug() << "unknown button clicked" << button;
        }
    }

    void handleAskMoveCheckBoxStateChange(int state) {
        qDebug() << "new ask move check box state" << state;

        // this alone unfortunately doesn't do the trick...
        for (auto* layout : {
            static_cast<QLayout*>(firstRunDialog->destDirVertLayout),
            static_cast<QLayout*>(firstRunDialog->destDirHorLayout),
        }) {
            layout->setEnabled(state > 0);
        }

        // have to also manually enable/disable all the
        for (auto* label : {
            static_cast<QWidget*>(firstRunDialog->destinationDirDescLabel),
            static_cast<QWidget*>(firstRunDialog->destinationDirLabel),
            static_cast<QWidget*>(firstRunDialog->customizeIntegrationDirButton),
        }) {
            label->setEnabled(state > 0);
        }
    }

    void handleCustomizeIntegrationDirButtonClicked(bool checked = false) {
        (void) checked;

        auto oldDir = destinationDir;
        if (oldDir.isEmpty())
            oldDir = integratedAppImagesDestination().absolutePath();

        auto newDir = QFileDialog::getExistingDirectory(this, tr("Choose integration destination dir"), oldDir);

        // the call above returns an empty string if the user aborts the dialog
        if (!newDir.isEmpty()) {
            destinationDir = newDir;
        }

        // updating never is a bad idea
        updateDestinationDirLabel();
    }

private:
    void updateDestinationDirLabel() {
        QString text = destinationDir;

        // fallback to default
        if (text.isEmpty())
            text = integratedAppImagesDestination().absolutePath() + " " + tr("(default)");

        firstRunDialog->destinationDirLabel->setText(text);
    }

    void initUi() {
        firstRunDialog = new Ui::FirstRunDialog;
        // setupUi will modify this dialog so that it looks just like what we designed in Qt Designer
        firstRunDialog->setupUi(this);

        // set up logo in a QLabel
        firstRunDialog->logoLabel->setText("");
        auto pixmap = QPixmap::fromImage(QImage(":/AppImageLauncher.svg")).scaled(QSize(128,128),
                Qt::KeepAspectRatio, Qt::SmoothTransformation
        );
        firstRunDialog->logoLabel->setPixmap(pixmap);

        // setting icon in Qt Designer doesn't seem to work
        firstRunDialog->customizeIntegrationDirButton->setIcon(this->style()->standardIcon(QStyle::SP_DirIcon));

        // reset defaults
        resetDefaults();

        // set up all connections
        connect(firstRunDialog->buttonBox, &QDialogButtonBox::clicked, this, &FirstRunDialog::handleButtonClicked);
        connect(firstRunDialog->askMoveCheckBox, &QCheckBox::stateChanged, this, &FirstRunDialog::handleAskMoveCheckBoxStateChange);

        connect(firstRunDialog->customizeIntegrationDirButton, &QPushButton::clicked, this, &FirstRunDialog::handleCustomizeIntegrationDirButtonClicked);
    }

public:
    FirstRunDialog() {
        initUi();
    }

    void writeConfigFile() {
        bool askToMove = firstRunDialog->askMoveCheckBox->checkState() == Qt::Checked;
        createConfigFile(askToMove ? 1 : 0, destinationDir, -1);
    }
};


void showFirstRunDialog() {
    auto dialog = new FirstRunDialog;

    setUpFallbackIconPaths(dialog);

    auto rv = dialog->exec();

    if (rv <= 0) {
        QApplication::exit(3);
        exit(3);
    }

    dialog->writeConfigFile();
}
07070100000039000081a400000000000000000000000168cf69400000001b000000000000000000000000000000000000001300000000src/ui/first-run.hvoid showFirstRunDialog();
0707010000003a000081a400000000000000000000000168cf6940000018c4000000000000000000000000000000000000001400000000src/ui/first-run.ui<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>FirstRunDialog</class>
 <widget class="QDialog" name="FirstRunDialog">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>532</width>
    <height>300</height>
   </rect>
  </property>
  <property name="sizePolicy">
   <sizepolicy hsizetype="MinimumExpanding" vsizetype="Expanding">
    <horstretch>0</horstretch>
    <verstretch>0</verstretch>
   </sizepolicy>
  </property>
  <property name="windowTitle">
   <string>First run</string>
  </property>
  <property name="sizeGripEnabled">
   <bool>true</bool>
  </property>
  <layout class="QGridLayout" name="gridLayout">
   <property name="sizeConstraint">
    <enum>QLayout::SetDefaultConstraint</enum>
   </property>
   <item row="0" column="0">
    <widget class="QLabel" name="logoLabel">
     <property name="sizePolicy">
      <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
       <horstretch>0</horstretch>
       <verstretch>0</verstretch>
      </sizepolicy>
     </property>
     <property name="minimumSize">
      <size>
       <width>130</width>
       <height>130</height>
      </size>
     </property>
     <property name="text">
      <string notr="true"/>
     </property>
     <property name="pixmap">
      <pixmap>:/AppImageLauncher.svg</pixmap>
     </property>
     <property name="scaledContents">
      <bool>false</bool>
     </property>
     <property name="alignment">
      <set>Qt::AlignHCenter|Qt::AlignTop</set>
     </property>
     <property name="margin">
      <number>8</number>
     </property>
    </widget>
   </item>
   <item row="0" column="1">
    <widget class="QLabel" name="welcomeTextLabel">
     <property name="sizePolicy">
      <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
       <horstretch>0</horstretch>
       <verstretch>0</verstretch>
      </sizepolicy>
     </property>
     <property name="minimumSize">
      <size>
       <width>0</width>
       <height>0</height>
      </size>
     </property>
     <property name="text">
      <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:11pt; font-weight:600;&quot;&gt;Welcome to AppImageLauncher!&lt;/span&gt;&lt;/p&gt;&lt;p&gt;This little helper is designed to improve your AppImage experience on your computer.&lt;/p&gt;&lt;p&gt;It appears you have never run AppImageLauncher before. Please take a minute and configure your preferences. You can always change these later on, using the control panel.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
     </property>
     <property name="alignment">
      <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
     </property>
     <property name="wordWrap">
      <bool>true</bool>
     </property>
     <property name="margin">
      <number>2</number>
     </property>
    </widget>
   </item>
   <item row="1" column="0" colspan="2">
    <widget class="QCheckBox" name="askMoveCheckBox">
     <property name="sizePolicy">
      <sizepolicy hsizetype="Minimum" vsizetype="Preferred">
       <horstretch>0</horstretch>
       <verstretch>0</verstretch>
      </sizepolicy>
     </property>
     <property name="text">
      <string>Ask me whether to move new AppImages into a central location</string>
     </property>
     <property name="checked">
      <bool>true</bool>
     </property>
    </widget>
   </item>
   <item row="2" column="0" colspan="2">
    <layout class="QVBoxLayout" name="destDirVertLayout">
     <property name="spacing">
      <number>0</number>
     </property>
     <item>
      <widget class="QLabel" name="destinationDirDescLabel">
       <property name="sizePolicy">
        <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
         <horstretch>0</horstretch>
         <verstretch>0</verstretch>
        </sizepolicy>
       </property>
       <property name="text">
        <string>Integration target destination directory:</string>
       </property>
      </widget>
     </item>
     <item>
      <layout class="QHBoxLayout" name="destDirHorLayout">
       <property name="sizeConstraint">
        <enum>QLayout::SetDefaultConstraint</enum>
       </property>
       <item>
        <widget class="QLabel" name="destinationDirLabel">
         <property name="font">
          <font>
           <family>DejaVu Sans Mono</family>
          </font>
         </property>
         <property name="text">
          <string notr="true">path placeholder</string>
         </property>
        </widget>
       </item>
       <item>
        <widget class="QPushButton" name="customizeIntegrationDirButton">
         <property name="sizePolicy">
          <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
           <horstretch>15</horstretch>
           <verstretch>0</verstretch>
          </sizepolicy>
         </property>
         <property name="minimumSize">
          <size>
           <width>100</width>
           <height>0</height>
          </size>
         </property>
         <property name="text">
          <string>Customize</string>
         </property>
        </widget>
       </item>
      </layout>
     </item>
    </layout>
   </item>
   <item row="3" column="0" colspan="2">
    <widget class="QDialogButtonBox" name="buttonBox">
     <property name="orientation">
      <enum>Qt::Horizontal</enum>
     </property>
     <property name="standardButtons">
      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::RestoreDefaults</set>
     </property>
     <property name="centerButtons">
      <bool>false</bool>
     </property>
    </widget>
   </item>
  </layout>
 </widget>
 <resources/>
 <connections>
  <connection>
   <sender>buttonBox</sender>
   <signal>accepted()</signal>
   <receiver>FirstRunDialog</receiver>
   <slot>accept()</slot>
   <hints>
    <hint type="sourcelabel">
     <x>248</x>
     <y>254</y>
    </hint>
    <hint type="destinationlabel">
     <x>157</x>
     <y>274</y>
    </hint>
   </hints>
  </connection>
  <connection>
   <sender>buttonBox</sender>
   <signal>rejected()</signal>
   <receiver>FirstRunDialog</receiver>
   <slot>reject()</slot>
   <hints>
    <hint type="sourcelabel">
     <x>316</x>
     <y>260</y>
    </hint>
    <hint type="destinationlabel">
     <x>286</x>
     <y>274</y>
    </hint>
   </hints>
  </connection>
 </connections>
</ui>
0707010000003b000081a400000000000000000000000168cf694000000755000000000000000000000000000000000000001e00000000src/ui/integration_dialog.cpp// system includes
#include <sstream>
#include <utility>

// library includes
#include <QStyle>

// local headers
#include "integration_dialog.h"
#include "ui_integration_dialog.h"

IntegrationDialog::IntegrationDialog(QString pathToAppImage, QString integratedAppImagesDestinationPath,
                                     QWidget* parent) :
    QDialog(parent), ui(new Ui::IntegrationDialog),
    pathToAppImage(std::move(pathToAppImage)),
    integratedAppImagesDestinationPath(std::move(integratedAppImagesDestinationPath)) {
    ui->setupUi(this);

    setIcon();
    setMessage();

    QObject::connect(ui->pushButtonIntegrateAndRun, &QPushButton::released, this,
                     &IntegrationDialog::onPushButtonIntegrateAndRunReleased);
    QObject::connect(ui->pushButtonRunOnce, &QPushButton::released, this,
                     &IntegrationDialog::onPushButtonRunOnceReleased);

    // make translation fit by adjusting the minimum size of the message label to the size calculated by Qt
    ui->message->setMinimumSize(ui->message->sizeHint());
}

void IntegrationDialog::setMessage() {
    QString message = ui->message->text();
    message = message.arg(pathToAppImage, integratedAppImagesDestinationPath);
    ui->message->setText(message);
}

void IntegrationDialog::setIcon() {
    QIcon icon = QIcon(":/AppImageLauncher.svg");
    QPixmap pixmap = icon.pixmap(QSize(64, 64));
    ui->icon->setPixmap(pixmap);
}

IntegrationDialog::~IntegrationDialog() {
    delete ui;
}

void IntegrationDialog::onPushButtonIntegrateAndRunReleased() {
    this->resultAction = ResultingAction::IntegrateAndRun;
    this->accept();
}

void IntegrationDialog::onPushButtonRunOnceReleased() {
    this->resultAction = ResultingAction::RunOnce;
    this->accept();
}

IntegrationDialog::ResultingAction IntegrationDialog::getResultAction() const {
    return resultAction;
}
0707010000003c000081a400000000000000000000000168cf6940000003b8000000000000000000000000000000000000001c00000000src/ui/integration_dialog.h#ifndef APPIMAGELAUNCHER_INTEGRATION_DIALOG_H
#define APPIMAGELAUNCHER_INTEGRATION_DIALOG_H

// library includes
#include <QDialog>

QT_BEGIN_NAMESPACE
namespace Ui { class IntegrationDialog; }
QT_END_NAMESPACE

class IntegrationDialog : public QDialog {
Q_OBJECT

public:
    enum ResultingAction {
        IntegrateAndRun,
        RunOnce
    };

    explicit IntegrationDialog(QString pathToAppImage, QString integratedAppImagesDestinationPath,
                               QWidget* parent = nullptr);

    ~IntegrationDialog() override;

    ResultingAction getResultAction() const;

protected:
    Q_SLOT void onPushButtonIntegrateAndRunReleased();

    Q_SLOT void onPushButtonRunOnceReleased();

    ResultingAction resultAction;

private:
    Ui::IntegrationDialog* ui;
    QString pathToAppImage;
    QString integratedAppImagesDestinationPath;

    void setIcon();

    void setMessage();
};

#endif //APPIMAGELAUNCHER_INTEGRATION_DIALOG_H
0707010000003d000081a400000000000000000000000168cf694000001379000000000000000000000000000000000000001d00000000src/ui/integration_dialog.ui<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>IntegrationDialog</class>
 <widget class="QDialog" name="IntegrationDialog">
  <property name="windowModality">
   <enum>Qt::WindowModal</enum>
  </property>
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>460</width>
    <height>300</height>
   </rect>
  </property>
  <property name="sizePolicy">
   <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
    <horstretch>0</horstretch>
    <verstretch>0</verstretch>
   </sizepolicy>
  </property>
  <property name="windowTitle">
   <string>Desktop Integration</string>
  </property>
  <property name="sizeGripEnabled">
   <bool>false</bool>
  </property>
  <property name="modal">
   <bool>true</bool>
  </property>
  <layout class="QVBoxLayout" name="verticalLayout">
   <item>
    <layout class="QHBoxLayout" name="horizontalLayout_2">
     <item>
      <widget class="QLabel" name="icon">
       <property name="sizePolicy">
        <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
         <horstretch>0</horstretch>
         <verstretch>0</verstretch>
        </sizepolicy>
       </property>
       <property name="text">
        <string>Icon</string>
       </property>
       <property name="alignment">
        <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
       </property>
       <property name="margin">
        <number>12</number>
       </property>
      </widget>
     </item>
     <item>
      <layout class="QVBoxLayout" name="verticalLayout_2">
       <item>
        <widget class="QLabel" name="message">
         <property name="sizePolicy">
          <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
           <horstretch>0</horstretch>
           <verstretch>0</verstretch>
          </sizepolicy>
         </property>
         <property name="text">
          <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Noto Sans'; font-size:10pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;%1 has not been integrated into your system.&lt;/p&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt; &lt;br /&gt;Integrating it will move the AppImage into a predefined location, and include it in your application launcher.&lt;/p&gt;
&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;To remove or update the AppImage, please use the context menu of the application icon in your task bar or launcher. &lt;/p&gt;
&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;The directory where the integrated AppImages are stored in is currently set to: %2&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
         </property>
         <property name="alignment">
          <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
         </property>
         <property name="wordWrap">
          <bool>true</bool>
         </property>
         <property name="margin">
          <number>0</number>
         </property>
        </widget>
       </item>
      </layout>
     </item>
    </layout>
   </item>
   <item>
    <layout class="QHBoxLayout" name="horizontalLayout">
     <item>
      <spacer name="horizontalSpacer">
       <property name="orientation">
        <enum>Qt::Horizontal</enum>
       </property>
       <property name="sizeHint" stdset="0">
        <size>
         <width>40</width>
         <height>20</height>
        </size>
       </property>
      </spacer>
     </item>
     <item>
      <widget class="QPushButton" name="pushButtonIntegrateAndRun">
       <property name="text">
        <string>Integrate and run</string>
       </property>
       <property name="default">
        <bool>true</bool>
       </property>
      </widget>
     </item>
     <item>
      <widget class="QPushButton" name="pushButtonRunOnce">
       <property name="text">
        <string>Run once</string>
       </property>
      </widget>
     </item>
    </layout>
   </item>
  </layout>
 </widget>
 <resources/>
 <connections/>
</ui>
0707010000003e000081a400000000000000000000000168cf69400000447b000000000000000000000000000000000000001000000000src/ui/main.cpp// system includes
#include <fstream>
#include <iostream>
#include <sstream>

extern "C" {
#include <sys/stat.h>
#include <libgen.h>
#include <unistd.h>
#include <glib.h>
}

// library includes
#include <QApplication>
#include <QCommandLineParser>
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QFileDialog>
#include <QFileInfo>
#include <QMessageBox>
#include <QProcess>
#include <QPushButton>
#include <QRegularExpression>
#include <QString>

extern "C" {
#include <appimage/appimage.h>
}

// local headers
#include "shared.h"
#include "trashbin.h"
#include "translationmanager.h"
#include "first-run.h"
#include "integration_dialog.h"

// Runs an AppImage. Returns suitable exit code for main application.
int runAppImage(const QString& pathToAppImage, unsigned long argc, char** argv) {
    // needs to be converted to std::string to be able to use c_str()
    // when using QString and then .toStdString().c_str(), the std::string instance will be an rvalue, and the
    // pointer returned by c_str() will be invalid
    auto fullPathToAppImage = QFileInfo(pathToAppImage).absoluteFilePath();

    auto type = appimage_get_type(fullPathToAppImage.toStdString().c_str(), false);
    if (type < 1 || type > 3) {
        displayError(QObject::tr("AppImageLauncher does not support type %1 AppImages at the moment.").arg(type));
        return 1;
    }

    // first of all, chmod +x the AppImage registerFile
    // be happy the registerFile is executable already
    if (!makeExecutable(fullPathToAppImage)) {
        displayError(QObject::tr("Could not make AppImage executable: %1").arg(fullPathToAppImage));
        return 1;
    }

    // suppress desktop integration script etc.
    setenv("DESKTOPINTEGRATION", "AppImageLauncher", true);

    auto makeVectorBuffer = [](const std::string& str) {
        std::vector<char> strBuffer(str.size() + 1, '\0');
        strncpy(strBuffer.data(), str.c_str(), str.size());
        return strBuffer;
    };

    // calculate buffer to bypass binary
    std::string pathToBinfmtBypassLauncher = privateLibDirPath("binfmt-bypass").toStdString() + "/binfmt-bypass";

    // create new args array for exec()d process
    std::vector<char*> args;

    // first argument is the path to our launcher
    auto pathToBinfmtBypassLauncherBuffer = makeVectorBuffer(pathToBinfmtBypassLauncher);
    args.push_back(pathToBinfmtBypassLauncherBuffer.data());

    // first argument is consumed by the bypass launcher
    // the reason we launch the bypass launcher as a new process to save RAM (we have to launch the actual AppImage
    // as a subprocess, and the launcher executable has a much lower memory footprint)
    auto pathToAppImageBuffer = makeVectorBuffer(pathToAppImage.toStdString());
    args.push_back(pathToAppImageBuffer.data());

    // copy arguments
    for (unsigned long i = 1; i < argc; i++) {
        args.push_back(argv[i]);
    }

    // args need to be null terminated
    args.push_back(nullptr);

    execv(pathToBinfmtBypassLauncher.c_str(), args.data());

    const auto& error = errno;
    std::cerr << QObject::tr("execv() failed: %1").arg(strerror(error)).toStdString() << std::endl;
    return 1;
}

// factory method to build and return a suitable Qt application instance
// it remembers a previously created instance, and will return it if available
// otherwise a new one is created and configure
// caution: cannot use <widget>.exec() any more, instead call <widget>.show() and use QApplication::exec()
QCoreApplication* getApp(char** argv) {
    if (QCoreApplication::instance() != nullptr)
        return QCoreApplication::instance();

    // build application version string
    std::string version;
    {
        std::ostringstream oss;
        oss << "version " << APPIMAGELAUNCHER_VERSION << " "
            << "(git commit " << APPIMAGELAUNCHER_GIT_COMMIT << "), built on "
            << APPIMAGELAUNCHER_BUILD_DATE;
        version = oss.str();
    }

    QCoreApplication* app;

    // need to pass rvalue, hence defining a variable
    int* fakeArgc = new int{1};

    static char** fakeArgv = new char*{strdup(argv[0])};

    if (isHeadless()) {
        app = new QCoreApplication(*fakeArgc, fakeArgv);
    } else {
        auto uiApp = new QApplication(*fakeArgc, fakeArgv);
        QApplication::setApplicationDisplayName("AppImageLauncher");

        // this doesn't seem to have any effect... but it doesn't hurt either
        uiApp->setWindowIcon(QIcon(":/AppImageLauncher.svg"));

        app = uiApp;
    }

    QCoreApplication::setApplicationName("AppImageLauncher");
    QCoreApplication::setApplicationVersion(QString::fromStdString(version));

    return app;
}

int main(int argc, char** argv) {
    // create a suitable application object (either graphical (QApplication) or headless (QCoreApplication))
    // Use a fake argc value to avoid QApplication from modifying the arguments
    QCoreApplication* app = getApp(argv);

    // install translations
    TranslationManager translationManager(*app);

    // clean up old desktop files
    if (!cleanUpOldDesktopIntegrationResources()) {
        displayError(QObject::tr("Failed to clean up old desktop files"));
        return 1;
    }

    // clean up trash directory
    {
        TrashBin bin;
        if (!bin.cleanUp()) {
            displayError(QObject::tr("Failed to clean up AppImage trash bin: %1").arg(bin.path()));
        }
    }

    std::ostringstream usage;
    usage << QObject::tr("Usage: %1 [options] <path>").arg(argv[0]).toStdString() << std::endl
          << QObject::tr("Desktop integration helper for AppImages, for use by Linux distributions.").toStdString()
          << std::endl
          << std::endl
          << QObject::tr("Options:").toStdString() << std::endl
          << "  --appimagelauncher-help     " << QObject::tr("Display this help and exit").toStdString() << std::endl
          << "  --appimagelauncher-version  " << QObject::tr("Display version and exit").toStdString() << std::endl
          << std::endl
          << QObject::tr("Arguments:").toStdString() << std::endl
          << "  path                        " << QObject::tr("Path to AppImage (mandatory)").toStdString() << std::endl;

    auto displayVersion = [&app]() {
        std::cerr << "AppImageLauncher " << app->applicationVersion().toStdString() << std::endl;
    };

    // display usage and exit if path to AppImage is missing
    if (argc <= 1) {
        displayVersion();
        std::cerr << std::endl;
        std::cerr << usage.str();
        return 1;
    }

    std::vector<char*> appImageArgv;
    // search for --appimagelauncher-* arguments in args list
    for (int i = 1; i < argc; i++) {
        QString arg = argv[i];

        // reserved argument space
        const QString prefix = "--appimagelauncher-";

        if (arg.startsWith(prefix)) {
            if (arg == prefix + "help") {
                displayVersion();
                std::cerr << std::endl;
                std::cerr << usage.str();
                return 0;
            } else if (arg == prefix + "version") {
                displayVersion();
                return 0;
            } else if (arg == prefix + "cleanup") {
                // exit immediately after cleanup
                return 0;
            } else {
                std::cerr << QObject::tr("Unknown AppImageLauncher option: %1").arg(arg).toStdString() << std::endl;
                return 1;
            }
        } else {
            appImageArgv.emplace_back(argv[i]);
        }
    }

    // sanitize path
    auto pathToAppImage = QDir(QString(argv[1])).absolutePath();

    if (!QFile(pathToAppImage).exists()) {
        displayError(QObject::tr("Error: no such file or directory: %1").arg(pathToAppImage));
        return 1;
    }

    // if the users wishes to disable AppImageLauncher, we just run the AppImage as-ish
    // also we don't ever want to integrate symlinks (see #290 for more information)
    if (getenv("APPIMAGELAUNCHER_DISABLE") != nullptr || QFileInfo(pathToAppImage).isSymLink()) {
        return runAppImage(pathToAppImage, appImageArgv.size(), appImageArgv.data());
    }

    const auto type = appimage_get_type(pathToAppImage.toStdString().c_str(), false);

    if (type <= 0 || type > 2) {
        displayError(QObject::tr("Not an AppImage: %1").arg(pathToAppImage));
        return 1;
    }

    // type 2 specific checks
    if (type == 2) {
        // check parameters
        {
            for (int i = 0; i < argc; i++) {
                QString arg = argv[i];

                // reserved argument space
                const QString prefix = "--appimage-";

                if (arg.startsWith(prefix)) {
                    // don't annoy users who try to mount or extract AppImages
                    if (arg == prefix + "mount" || arg == prefix + "extract" || arg == prefix + "updateinformation") {
                        return runAppImage(pathToAppImage, appImageArgv.size(), appImageArgv.data());
                    }
                }
            }
        }
    }

    // enable and start/disable and stop appimagelauncherd service
    auto config = getConfig();

    // assumes defaults if config doesn't exist or lacks the related key(s)
    if (config == nullptr || !config->contains("AppImageLauncher/enable_daemon") ||
        config->value("AppImageLauncher/enable_daemon").toBool()) {
        system("systemctl --user enable appimagelauncherd.service");
        system("systemctl --user start  appimagelauncherd.service");
    } else {
        system("systemctl --user disable appimagelauncherd.service");
        system("systemctl --user stop    appimagelauncherd.service");
    }

    // beyond the next block, the code requires a UI
    // as we don't want to offer integration over a headless connection, we just run the AppImage
    if (isHeadless()) {
        return runAppImage(pathToAppImage, appImageArgv.size(), appImageArgv.data());
    }

    // if config doesn't exist, create a default one
    if (config == nullptr) {
        showFirstRunDialog();
        config = getConfig();
    }

    if (config == nullptr) {
        displayError("Could not read config file");
    }

    // if the user opted out of the "ask move" thing, we can just run the AppImage
    if (config->contains("AppImageLauncher/ask_to_move") && !config->value("AppImageLauncher/ask_to_move").toBool()) {
        return runAppImage(pathToAppImage, appImageArgv.size(), appImageArgv.data());
    }

    // check for X-AppImage-Integrate=false
    auto shallNotBeIntegrated = appimage_shall_not_be_integrated(pathToAppImage.toStdString().c_str());
    if (shallNotBeIntegrated < 0)
        std::cerr << "AppImageLauncher error: appimage_shall_not_be_integrated() failed (returned "
                  << shallNotBeIntegrated << ")" << std::endl;
    else if (shallNotBeIntegrated > 0)
        return runAppImage(pathToAppImage, appImageArgv.size(), appImageArgv.data());

    // AppImages in AppImages are not supposed to be integrated
    if (pathToAppImage.startsWith("/tmp/.mount_"))
        return runAppImage(pathToAppImage, appImageArgv.size(), appImageArgv.data());

    // ignore terminal apps (fixes #2)
    auto isTerminalApp = appimage_is_terminal_app(pathToAppImage.toStdString().c_str());
    if (isTerminalApp < 0)
        std::cerr << "AppImageLauncher error: appimage_is_terminal_app() failed (returned " << isTerminalApp << ")"
                  << std::endl;
    else if (isTerminalApp > 0)
        return runAppImage(pathToAppImage, appImageArgv.size(), appImageArgv.data());

    // AppImages in AppImages are not supposed to be integrated
    if (pathToAppImage.startsWith("/tmp/.mount_"))
        return runAppImage(pathToAppImage, appImageArgv.size(), appImageArgv.data());

    const auto pathToIntegratedAppImage = buildPathToIntegratedAppImage(pathToAppImage);

    auto integrateAndRunAppImage = [&pathToAppImage, &pathToIntegratedAppImage, &appImageArgv]() {
        // check whether integration was successful
        auto rv = integrateAppImage(pathToAppImage, pathToIntegratedAppImage);

        // make sure the icons in the launcher are refreshed
        if (!updateDesktopDatabaseAndIconCaches())
            return 1;

        if (rv == INTEGRATION_FAILED) {
            return 1;
        } else if (rv == INTEGRATION_ABORTED) {
            return runAppImage(pathToAppImage, appImageArgv.size(), appImageArgv.data());
        } else {
            return runAppImage(pathToIntegratedAppImage, appImageArgv.size(), appImageArgv.data());
        }
    };

    // after checking whether the AppImage can/must be run without integrating it, we now check whether it actually
    // has been integrated already
    if (hasAlreadyBeenIntegrated(pathToAppImage)) {
        auto updateAndRunAppImage = [&pathToAppImage, &appImageArgv]() {
            // in case there was an update of AppImageLauncher, we should should also update the desktop database
            // and icon caches
            if (!desktopFileHasBeenUpdatedSinceLastUpdate(pathToAppImage)) {
                if (!updateDesktopFileAndIcons(pathToAppImage))
                    return 1;

                // make sure the icons in the launcher are refreshed after updating the desktop file
                if (!updateDesktopDatabaseAndIconCaches())
                    return 1;
            }
            return runAppImage(pathToAppImage, appImageArgv.size(), appImageArgv.data());
        };

        // assume we have to ask
        // prove me wrong!
        bool needToAskAboutMoving = true;

        // okay, I'll try to prove you wrong
        {
            auto directoriesNotToAskAboutMovingFor = daemonDirectoriesToWatch(config);

            // normally the main integration destination should be contained
            // but bugs happen, and we want to be sure not to create a weird situation where you'd be asked about
            // moving files into yet the directory you want to move them into
            directoriesNotToAskAboutMovingFor.insert(integratedAppImagesDestination());

            for (const auto& dir : directoriesNotToAskAboutMovingFor) {
                if (isInDirectory(pathToAppImage, dir)) {
                    needToAskAboutMoving = false;
                    break;
                }
            }
        }

        // not so fast: even if it's not in the main integration directory, there's more viable locations where
        // AppImages may reside just fine
        if (needToAskAboutMoving) {
            for (const auto& additionalLocation : additionalAppImagesLocations()) {
                if (isInDirectory(pathToAppImage, additionalLocation)) {
                    needToAskAboutMoving = false;
                }
            }
        }

        if (needToAskAboutMoving) {
            auto* messageBox = new QMessageBox(
                QMessageBox::Warning,
                QMessageBox::tr("Warning"),
                QMessageBox::tr("AppImage %1 has already been integrated, but it is not in the current integration "
                                "destination directory."
                                "\n\n"
                                "Do you want to move it into the new destination?"
                                "\n\n"
                                "Choosing No will run the AppImage once, and leave the AppImage in its current "
                                "directory."
                                "\n\n").arg(pathToAppImage) +
                // translate separately to share string with the other dialog
                QObject::tr("The directory the integrated AppImages are stored in is currently set to:\n"
                            "%1").arg(integratedAppImagesDestination().path()) + "\n",
                QMessageBox::Yes | QMessageBox::No
            );

            messageBox->setDefaultButton(QMessageBox::Yes);
            messageBox->show();

            QApplication::exec();

            // if the user selects No, then continue as if the AppImage would not be in this directory
            if (messageBox->clickedButton() == messageBox->button(QMessageBox::Yes)) {
                // unregister AppImage, move, and re-integrate
                if (appimage_unregister_in_system(pathToAppImage.toStdString().c_str(), false) != 0) {
                    displayError(QMessageBox::tr("Failed to unregister AppImage before re-integrating it"));
                    return 1;
                }

                return integrateAndRunAppImage();
            } else {
                return updateAndRunAppImage();
            }
        } else {
            return updateAndRunAppImage();
        }
    }

    QString integratedAppImagesDestinationPath = integratedAppImagesDestination().path();
    auto integrationDialog = new IntegrationDialog(pathToAppImage, integratedAppImagesDestinationPath);
    integrationDialog->show();

    // As the integration dialog is the only window in our application we can safely use its exec method
    integrationDialog->exec();

    if (integrationDialog->result() == QDialog::Rejected)
        return 0;

    switch (integrationDialog->getResultAction()) {
        case IntegrationDialog::IntegrateAndRun:
            return integrateAndRunAppImage();
        case IntegrationDialog::RunOnce:
            return runAppImage(pathToAppImage, appImageArgv.size(), appImageArgv.data());
        default:
            displayError(QObject::tr("Unexpected result from the integration dialog."));
            return 1;
    }
}

0707010000003f000081a400000000000000000000000168cf694000000b1c000000000000000000000000000000000000001100000000src/ui/remove.ui<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>RemoveDialog</class>
 <widget class="QDialog" name="RemoveDialog">
  <property name="windowModality">
   <enum>Qt::WindowModal</enum>
  </property>
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>415</width>
    <height>96</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Delete AppImage</string>
  </property>
  <property name="locale">
   <locale language="English" country="UnitedStates"/>
  </property>
  <layout class="QGridLayout" name="gridLayout">
   <item row="0" column="0">
    <layout class="QVBoxLayout" name="verticalLayout">
     <property name="sizeConstraint">
      <enum>QLayout::SetMinimumSize</enum>
     </property>
     <item>
      <widget class="QLabel" name="messageLabel">
       <property name="text">
        <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Are you sure you want to delete this AppImage?&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
       </property>
       <property name="alignment">
        <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
       </property>
      </widget>
     </item>
     <item>
      <widget class="QLabel" name="pathLabel">
       <property name="sizePolicy">
        <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
         <horstretch>0</horstretch>
         <verstretch>0</verstretch>
        </sizepolicy>
       </property>
       <property name="minimumSize">
        <size>
         <width>389</width>
         <height>0</height>
        </size>
       </property>
       <property name="text">
        <string>%1</string>
       </property>
       <property name="wordWrap">
        <bool>true</bool>
       </property>
      </widget>
     </item>
     <item>
      <widget class="QDialogButtonBox" name="buttonBox">
       <property name="orientation">
        <enum>Qt::Horizontal</enum>
       </property>
       <property name="standardButtons">
        <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
       </property>
      </widget>
     </item>
    </layout>
   </item>
  </layout>
 </widget>
 <resources/>
 <connections>
  <connection>
   <sender>buttonBox</sender>
   <signal>accepted()</signal>
   <receiver>RemoveDialog</receiver>
   <slot>accept()</slot>
   <hints>
    <hint type="sourcelabel">
     <x>248</x>
     <y>254</y>
    </hint>
    <hint type="destinationlabel">
     <x>157</x>
     <y>274</y>
    </hint>
   </hints>
  </connection>
  <connection>
   <sender>buttonBox</sender>
   <signal>rejected()</signal>
   <receiver>RemoveDialog</receiver>
   <slot>reject()</slot>
   <hints>
    <hint type="sourcelabel">
     <x>316</x>
     <y>260</y>
    </hint>
    <hint type="destinationlabel">
     <x>286</x>
     <y>274</y>
    </hint>
   </hints>
  </connection>
 </connections>
</ui>
07070100000040000081a400000000000000000000000168cf6940000010c6000000000000000000000000000000000000001700000000src/ui/remove_main.cpp// system includes
#include <iostream>
#include <sstream>

// library includes
#include <QApplication>
#include <QCommandLineParser>
#include <QDir>
#include <QFile>
#include <QFileDialog>
#include <QFileInfo>
#include <QLibraryInfo>
#include <QMessageBox>
#include <QObject>
#include <QPushButton>
#include <QTranslator>
extern "C" {
    #include <appimage/appimage.h>
}

// local includes
#include "shared.h"
#include "translationmanager.h"
#include "trashbin.h"
#include "ui_remove.h"

int main(int argc, char** argv) {
    QCommandLineParser parser;
    parser.setApplicationDescription(QObject::tr("Helper to delete integrated AppImages easily, e.g., from the application launcher's context menu"));
    QApplication app(argc, argv);
    QApplication::setApplicationDisplayName("AppImageLauncher");
    QApplication::setWindowIcon(QIcon(":/AppImageLauncher.svg"));

    std::ostringstream version;
    version << "version " << APPIMAGELAUNCHER_VERSION << " "
            << "(git commit " << APPIMAGELAUNCHER_GIT_COMMIT << "), built on "
            << APPIMAGELAUNCHER_BUILD_DATE;
    QApplication::setApplicationVersion(QString::fromStdString(version.str()));

    // install translations
    TranslationManager translationManager(app);

    parser.addHelpOption();
    parser.addVersionOption();

    parser.process(app);

    parser.addPositionalArgument("path", QObject::tr("Path to AppImage"), QObject::tr("<path>"));

    if (parser.positionalArguments().empty()) {
        parser.showHelp(1);
    }

    const auto pathToAppImage = parser.positionalArguments().first();

    if (!QFile(pathToAppImage).exists()) {
        QMessageBox::critical(nullptr, "Error", QObject::tr("Error: no such file or directory: %1").arg(pathToAppImage));
        return 1;
    }

    checkAuthorizationAndShowDialogIfNecessary(pathToAppImage, "Delete anyway?");

    const auto type = appimage_get_type(pathToAppImage.toStdString().c_str(), false);

    if (type <= 0 || type > 2) {
        QMessageBox::critical(
            nullptr,
            QObject::tr("AppImage delete helper error"),
            QObject::tr("Not an AppImage:\n\n%1").arg(pathToAppImage)
        );
        return 1;
    }

    // this tool should not do anything if the file isn't integrated
    // the file is only supposed to work on integrated AppImages and _nothing else_
//    if (!hasAlreadyBeenIntegrated(pathToAppImage)) {
//        QMessageBox::critical(
//                nullptr,
//                QObject::tr("AppImage delete helper error"),
//                QObject::tr("Refusing to work on non-integrated AppImage:\n\n%1").arg(pathToAppImage)
//        );
//        return 1;
//    }

    QDialog dialog;
    Ui::RemoveDialog ui;

    ui.setupUi(&dialog);
    ui.pathLabel->setText(pathToAppImage);

    // must be done *after* loading the UI into the dialog
    setUpFallbackIconPaths(&dialog);

    auto rv = dialog.exec();

    // check if user has canceled the dialog
    // a confirmation would result in an exit code of 1
    if (rv != 1) {
        return 0;
    }

    // first, unregister AppImage
    if (!unregisterAppImage(pathToAppImage)) {
        QMessageBox::critical(
                nullptr,
                QObject::tr("Error"),
                QObject::tr("Failed to unregister AppImage: %1").arg(pathToAppImage)
        );
        return 1;
    }

    TrashBin bin;

    // now, move AppImage into trash bin
    if (!bin.disposeAppImage(pathToAppImage)) {
        QMessageBox::critical(
                nullptr,
                QObject::tr("Error"),
                QObject::tr("Failed to move AppImage into trash bin directory")
        );
        return 1;
    }

    // run clean up cycle for trash bin
    // if the current AppImage is ready to be deleted, this call will immediately remove it from the system
    // otherwise, it'll be cleaned up at some subsequent run of AppImageLauncher or the removal tool
    if (!bin.cleanUp()) {
        QMessageBox::critical(
                nullptr,
                QObject::tr("Error"),
                QObject::tr("Failed to clean up AppImage trash bin: %1").arg(bin.path())
        );
        return 1;
    }

    // update desktop database and icon caches
    if (!updateDesktopDatabaseAndIconCaches())
        return 1;

    return 0;
}

07070100000041000081a400000000000000000000000168cf694000000101000000000000000000000000000000000000001500000000src/ui/resources.qrc<!DOCTYPE RCC>
<RCC version="1.0">
    <qresource>
        <file alias="AppImageLauncher.svg">../../resources/icons/hicolor/scalable/apps/AppImageLauncher.svg</file>
        <file alias="update_spinner.qml">update_spinner.qml</file>
    </qresource>
</RCC>
07070100000042000081a400000000000000000000000168cf69400000210d000000000000000000000000000000000000001b00000000src/ui/settings_dialog.cpp// libraries
#include <QDebug>
#include <QFileDialog>
#include <QFileIconProvider>
#include <QStandardPaths>

// local
#include "settings_dialog.h"
#include "ui_settings_dialog.h"
#include "shared.h"

SettingsDialog::SettingsDialog(QWidget* parent) : QDialog(parent), ui(new Ui::SettingsDialog),
                                                  settingsFile(getConfig(this)) {
    ui->setupUi(this);

    ui->applicationsDirLineEdit->setPlaceholderText(integratedAppImagesDestination().absolutePath());

    loadSettings();

// cosmetic changes in lite mode
#ifdef BUILD_LITE
    ui->daemonIsEnabledCheckBox->setChecked(true);
    ui->daemonIsEnabledCheckBox->setEnabled(false);

    ui->askMoveCheckBox->setChecked(false);
    ui->askMoveCheckBox->setEnabled(false);
#endif

    connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &SettingsDialog::onDialogAccepted);
    connect(ui->chooseAppsDirToolButton, &QToolButton::released, this, &SettingsDialog::onChooseAppsDirClicked);
    connect(ui->additionalDirsAddButton, &QToolButton::released, this, &SettingsDialog::onAddDirectoryToWatchButtonClicked);
    connect(ui->additionalDirsRemoveButton, &QToolButton::released, this, &SettingsDialog::onRemoveDirectoryToWatchButtonClicked);
    connect(ui->additionalDirsListWidget, &QListWidget::itemActivated, this, &SettingsDialog::onDirectoryToWatchItemActivated);
    connect(ui->additionalDirsListWidget, &QListWidget::itemClicked, this, &SettingsDialog::onDirectoryToWatchItemActivated);

    QStringList availableFeatures;

#ifdef ENABLE_UPDATE_HELPER
    availableFeatures << "<span style='color: green;'>✔</span> " + tr("updater available for AppImages supporting AppImageUpdate");
#else
    availableFeatures << "<span style='color: red;'>🞬</span> " + tr("updater unavailable");
#endif

#ifdef BUILD_LITE
    availableFeatures << "<br /><br />"
                      << tr("<strong>Note: this is an AppImageLauncher Lite build, only supports a limited set of features</strong><br />"
                            "Please install the full version via the provided native packages to enjoy the full AppImageLauncher experience");
#endif

    ui->featuresLabel->setText(availableFeatures.join('\n'));

    // no matter what tab was selected when saving in Qt designer, we want to start up with the first tab
    ui->tabWidget->setCurrentWidget(ui->launcherTab);
}

SettingsDialog::~SettingsDialog() {
    delete ui;
}

void SettingsDialog::addDirectoryToWatchToListView(const QString& dirPath) {
    // empty paths are not permitted
    if (dirPath.isEmpty())
        return;

    const QDir dir(dirPath);

    // we don't want to redundantly add the main integration directory
    if (dir == integratedAppImagesDestination())
        return;

    QIcon icon;

    auto findIcon = [](const std::initializer_list<QString>& names) {
        for (const auto& i : names) {
            auto icon = QIcon::fromTheme(i, loadIconWithFallback(i));

            if (!icon.isNull())
                return icon;
        }

        return QIcon{};
    };

    if (dir.exists()) {
        icon = findIcon({"folder"});
    } else {
        // TODO: search for more meaningful icon, "remove" doesn't really show the directory is missing
        icon = findIcon({"remove"});
    }

    if (icon.isNull()) {
        qDebug() << "item icon unavailable, using fallback";
    }

    auto* item = new QListWidgetItem(icon, dirPath);
    ui->additionalDirsListWidget->addItem(item);
}

void SettingsDialog::loadSettings() {
    const auto daemonIsEnabled = settingsFile->value("AppImageLauncher/enable_daemon", "true").toBool();
    const auto askMoveChecked = settingsFile->value("AppImageLauncher/ask_to_move", "true").toBool();

    if (settingsFile) {
        ui->daemonIsEnabledCheckBox->setChecked(daemonIsEnabled);
        ui->askMoveCheckBox->setChecked(askMoveChecked);
        ui->applicationsDirLineEdit->setText(settingsFile->value("AppImageLauncher/destination").toString());

        const auto additionalDirsPath = settingsFile->value("appimagelauncherd/additional_directories_to_watch", "").toString();
        for (const auto& dirPath : additionalDirsPath.split(":")) {
            addDirectoryToWatchToListView(dirPath);
        }
    }
}

void SettingsDialog::onDialogAccepted() {
    saveSettings();
    toggleDaemon();
}

void SettingsDialog::saveSettings() {
    QStringList additionalDirsToWatch;

    {
        QListWidgetItem* currentItem;

        for (int i = 0; (currentItem = ui->additionalDirsListWidget->item(i)) != nullptr; ++i) {
            additionalDirsToWatch << currentItem->text();
        }
    }

    // temporary workaround to fill in the monitorMountedFilesystems with the same value it had in the old settings
    // this is supposed to support the option while hiding it in the settings
    int monitorMountedFilesystems = -1;
    {
        const auto oldSettings = getConfig();

        static constexpr auto oldKey = "appimagelauncherd/monitor_mounted_filesystems";

        // getConfig might return a null pointer if the config file doesn't exist
        // we have to handle this, obviously
        if (oldSettings != nullptr && oldSettings->contains(oldKey)) {
            const auto oldValue = oldSettings->value(oldKey).toBool();
            monitorMountedFilesystems = oldValue ? 1 : 0;
        }
    }

    createConfigFile(ui->askMoveCheckBox->isChecked(),
                     ui->applicationsDirLineEdit->text(),
                     ui->daemonIsEnabledCheckBox->isChecked(),
                     additionalDirsToWatch,
                     monitorMountedFilesystems);

    // reload settings
    loadSettings();
}

void SettingsDialog::toggleDaemon() {
    // assumes defaults if config doesn't exist or lacks the related key(s)
    if (settingsFile) {
        if (settingsFile->value("AppImageLauncher/enable_daemon", "true").toBool()) {
            system("systemctl --user enable  appimagelauncherd.service");
            // we want to actually restart the service to apply the new configuration
            system("systemctl --user restart appimagelauncherd.service");
        } else {
            system("systemctl --user disable appimagelauncherd.service");
            system("systemctl --user stop    appimagelauncherd.service");
        }
    }
}

void SettingsDialog::onChooseAppsDirClicked() {
    QFileDialog fileDialog(this);

    fileDialog.setFileMode(QFileDialog::DirectoryOnly);
    fileDialog.setWindowTitle(tr("Select Applications directory"));
    fileDialog.setDirectory(integratedAppImagesDestination().absolutePath());

    // Gtk+ >= 3 segfaults when trying to use the native dialog, therefore we need to enforce the Qt one
    // See #218 for more information
    fileDialog.setOption(QFileDialog::DontUseNativeDialog, true);

    if (fileDialog.exec()) {
        QString dirPath = fileDialog.selectedFiles().first();
        ui->applicationsDirLineEdit->setText(dirPath);
    }
}

void SettingsDialog::onAddDirectoryToWatchButtonClicked() {
    QFileDialog fileDialog(this);

    fileDialog.setFileMode(QFileDialog::DirectoryOnly);
    fileDialog.setWindowTitle(tr("Select additional directory to watch"));
    fileDialog.setDirectory(QStandardPaths::locate(QStandardPaths::HomeLocation, ".", QStandardPaths::LocateDirectory));

    // Gtk+ >= 3 segfaults when trying to use the native dialog, therefore we need to enforce the Qt one
    // See #218 for more information
    fileDialog.setOption(QFileDialog::DontUseNativeDialog, true);

    if (fileDialog.exec()) {
        QString dirPath = fileDialog.selectedFiles().first();
        addDirectoryToWatchToListView(dirPath);
    }
}

void SettingsDialog::onRemoveDirectoryToWatchButtonClicked() {
    auto* widget = ui->additionalDirsListWidget;

    auto* currentItem = widget->currentItem();

    if (currentItem == nullptr)
        return;

    const auto index = widget->row(currentItem);

    // after taking it, we have to delete it ourselves, Qt docs say
    auto deletedItem = widget->takeItem(index);
    delete deletedItem;

    // we should deactivate the remove button once the last item is gone
    if (widget->item(0) == nullptr) {
        ui->additionalDirsRemoveButton->setEnabled(false);
    }
}

void SettingsDialog::onDirectoryToWatchItemActivated(QListWidgetItem* item) {
    // we activate the button based on whether there's an item selected
    ui->additionalDirsRemoveButton->setEnabled(item != nullptr);
}
07070100000043000081a400000000000000000000000168cf694000000306000000000000000000000000000000000000001900000000src/ui/settings_dialog.h#pragma once

// system
#include <memory>

// libraries
#include <QDialog>
#include <QListWidgetItem>
#include <QSettings>


namespace Ui {
    class SettingsDialog;
}

class SettingsDialog : public QDialog {
Q_OBJECT

public:
    explicit SettingsDialog(QWidget* parent = nullptr);

    ~SettingsDialog() override;

protected slots:
    void onChooseAppsDirClicked();
    void onAddDirectoryToWatchButtonClicked();
    void onRemoveDirectoryToWatchButtonClicked();
    void onDirectoryToWatchItemActivated(QListWidgetItem* item);

    void onDialogAccepted();

private:
    void loadSettings();

    void saveSettings();

    void toggleDaemon();

    void addDirectoryToWatchToListView(const QString& dirPath);

    Ui::SettingsDialog* ui;
    QSettings* settingsFile;
};
07070100000044000081a400000000000000000000000168cf694000002573000000000000000000000000000000000000001a00000000src/ui/settings_dialog.ui<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>SettingsDialog</class>
 <widget class="QDialog" name="SettingsDialog">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>539</width>
    <height>459</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>AppImageLauncher Settings</string>
  </property>
  <layout class="QVBoxLayout" name="verticalLayout">
   <item>
    <widget class="QTabWidget" name="tabWidget">
     <property name="currentIndex">
      <number>1</number>
     </property>
     <widget class="QWidget" name="launcherTab">
      <attribute name="title">
       <string notr="true">AppImageLauncher</string>
      </attribute>
      <layout class="QVBoxLayout" name="verticalLayout_4">
       <item>
        <widget class="QGroupBox" name="launcherGroupBox">
         <property name="title">
          <string>Launcher Dialog</string>
         </property>
         <layout class="QVBoxLayout" name="verticalLayout_2">
          <item>
           <widget class="QCheckBox" name="askMoveCheckBox">
            <property name="text">
             <string>Ask whether to move AppImage files into the applications directory</string>
            </property>
           </widget>
          </item>
         </layout>
        </widget>
       </item>
       <item>
        <widget class="QGroupBox" name="integrationDirectoryGroupBox">
         <property name="title">
          <string>Location where to store your AppImage files to ease their management</string>
         </property>
         <layout class="QHBoxLayout" name="horizontalLayout_2">
          <item>
           <layout class="QHBoxLayout" name="integrationGroupBoxLayout">
            <item>
             <widget class="QLineEdit" name="applicationsDirLineEdit">
              <property name="text">
               <string notr="true"/>
              </property>
              <property name="placeholderText">
               <string>Applications directory path</string>
              </property>
             </widget>
            </item>
            <item>
             <widget class="QToolButton" name="chooseAppsDirToolButton">
              <property name="text">
               <string/>
              </property>
              <property name="icon">
               <iconset theme="folder">
                <normaloff>.</normaloff>.</iconset>
              </property>
             </widget>
            </item>
           </layout>
          </item>
         </layout>
        </widget>
       </item>
       <item>
        <widget class="QGroupBox" name="availableFeaturesGroupBox">
         <property name="sizePolicy">
          <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
           <horstretch>0</horstretch>
           <verstretch>0</verstretch>
          </sizepolicy>
         </property>
         <property name="minimumSize">
          <size>
           <width>0</width>
           <height>80</height>
          </size>
         </property>
         <property name="locale">
          <locale language="English" country="UnitedStates"/>
         </property>
         <property name="title">
          <string>Available Features</string>
         </property>
         <layout class="QVBoxLayout" name="verticalLayout_3">
          <item>
           <widget class="QLabel" name="featuresLabel">
            <property name="sizePolicy">
             <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
              <horstretch>0</horstretch>
              <verstretch>0</verstretch>
             </sizepolicy>
            </property>
            <property name="minimumSize">
             <size>
              <width>0</width>
              <height>0</height>
             </size>
            </property>
            <property name="layoutDirection">
             <enum>Qt::LeftToRight</enum>
            </property>
            <property name="text">
             <string notr="true">TextLabel</string>
            </property>
            <property name="alignment">
             <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
            </property>
            <property name="wordWrap">
             <bool>true</bool>
            </property>
           </widget>
          </item>
         </layout>
        </widget>
       </item>
       <item>
        <spacer name="verticalSpacer">
         <property name="orientation">
          <enum>Qt::Vertical</enum>
         </property>
         <property name="sizeHint" stdset="0">
          <size>
           <width>20</width>
           <height>40</height>
          </size>
         </property>
        </spacer>
       </item>
      </layout>
     </widget>
     <widget class="QWidget" name="daemonTab">
      <attribute name="title">
       <string notr="true">appimagelauncherd</string>
      </attribute>
      <layout class="QVBoxLayout" name="verticalLayout_5">
       <item>
        <widget class="QGroupBox" name="daemonGeneralSettingsGroupBox">
         <property name="locale">
          <locale language="English" country="UnitedStates"/>
         </property>
         <property name="title">
          <string>General settings</string>
         </property>
         <layout class="QVBoxLayout" name="verticalLayout_6">
          <item>
           <widget class="QCheckBox" name="daemonIsEnabledCheckBox">
            <property name="toolTip">
             <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;When this box is checked, AppImageLauncher automatically starts a daemon called appimagelauncherd.&lt;/p&gt;&lt;p&gt;This daemon automatically integrates AppImages you copy into the &amp;quot;Applications directory&amp;quot; and the additional directories you configured. When the files are deleted, the daemon will clean up the integration data.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
            </property>
            <property name="text">
             <string>Auto start auto-integration daemon</string>
            </property>
           </widget>
          </item>
         </layout>
        </widget>
       </item>
       <item>
        <widget class="QGroupBox" name="additionalDirsGroupBox">
         <property name="locale">
          <locale language="English" country="UnitedStates"/>
         </property>
         <property name="title">
          <string>Additional directories to watch</string>
         </property>
         <layout class="QHBoxLayout" name="horizontalLayout_3">
          <item>
           <widget class="QListWidget" name="additionalDirsListWidget"/>
          </item>
          <item>
           <layout class="QVBoxLayout" name="additionalDirsButtonsLayout">
            <item>
             <widget class="QToolButton" name="additionalDirsAddButton">
              <property name="toolTip">
               <string>Add new directory to list</string>
              </property>
              <property name="icon">
               <iconset theme="list-add">
                <normaloff>.</normaloff>.</iconset>
              </property>
             </widget>
            </item>
            <item>
             <widget class="QToolButton" name="additionalDirsRemoveButton">
              <property name="enabled">
               <bool>false</bool>
              </property>
              <property name="toolTip">
               <string>Remove selected directory from list</string>
              </property>
              <property name="icon">
               <iconset theme="list-remove">
                <normaloff>.</normaloff>.</iconset>
              </property>
             </widget>
            </item>
            <item>
             <spacer name="verticalSpacer_2">
              <property name="orientation">
               <enum>Qt::Vertical</enum>
              </property>
              <property name="sizeHint" stdset="0">
               <size>
                <width>20</width>
                <height>40</height>
               </size>
              </property>
             </spacer>
            </item>
           </layout>
          </item>
         </layout>
        </widget>
       </item>
      </layout>
     </widget>
    </widget>
   </item>
   <item>
    <spacer name="verticalSpacer_3">
     <property name="orientation">
      <enum>Qt::Vertical</enum>
     </property>
     <property name="sizeType">
      <enum>QSizePolicy::Minimum</enum>
     </property>
     <property name="sizeHint" stdset="0">
      <size>
       <width>20</width>
       <height>10</height>
      </size>
     </property>
    </spacer>
   </item>
   <item>
    <widget class="QDialogButtonBox" name="buttonBox">
     <property name="orientation">
      <enum>Qt::Horizontal</enum>
     </property>
     <property name="standardButtons">
      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
     </property>
    </widget>
   </item>
  </layout>
 </widget>
 <resources/>
 <connections>
  <connection>
   <sender>buttonBox</sender>
   <signal>accepted()</signal>
   <receiver>SettingsDialog</receiver>
   <slot>accept()</slot>
   <hints>
    <hint type="sourcelabel">
     <x>248</x>
     <y>254</y>
    </hint>
    <hint type="destinationlabel">
     <x>157</x>
     <y>274</y>
    </hint>
   </hints>
  </connection>
  <connection>
   <sender>buttonBox</sender>
   <signal>rejected()</signal>
   <receiver>SettingsDialog</receiver>
   <slot>reject()</slot>
   <hints>
    <hint type="sourcelabel">
     <x>316</x>
     <y>260</y>
    </hint>
    <hint type="destinationlabel">
     <x>286</x>
     <y>274</y>
    </hint>
   </hints>
  </connection>
 </connections>
</ui>
07070100000045000081a400000000000000000000000168cf69400000026e000000000000000000000000000000000000001900000000src/ui/settings_main.cpp// libraries
#include <QApplication>

// local
#include <translationmanager.h>
#include <shared.h>
#include "settings_dialog.h"

int main(int argc, char** argv) {
    QApplication app(argc, argv);
    QApplication::setApplicationDisplayName("AppImageLauncher Settings");
    QApplication::setWindowIcon(QIcon(":/AppImageLauncher.svg"));

    TranslationManager mgr(app);
//
//    // we ship some very basic fallbacks for icons used in the settings dialog
//    // this should fix missing icons on some distros

    SettingsDialog dialog;

    setUpFallbackIconPaths(&dialog);

    dialog.show();

    return app.exec();
}
07070100000046000081a400000000000000000000000168cf694000000c7f000000000000000000000000000000000000001100000000src/ui/update.ui<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>UpdateDialog</class>
 <widget class="QDialog" name="UpdateDialog">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>400</width>
    <height>168</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Dialog</string>
  </property>
  <layout class="QVBoxLayout" name="verticalLayout">
   <property name="leftMargin">
    <number>0</number>
   </property>
   <property name="topMargin">
    <number>0</number>
   </property>
   <property name="rightMargin">
    <number>0</number>
   </property>
   <property name="bottomMargin">
    <number>0</number>
   </property>
   <item>
    <widget class="QStackedWidget" name="stackedWidget">
     <property name="currentIndex">
      <number>2</number>
     </property>
     <widget class="QWidget" name="spinnerPage">
      <layout class="QVBoxLayout" name="verticalLayout_2" stretch="1,0">
       <item>
        <widget class="QQuickWidget" name="spinnerQuickWidget">
         <property name="resizeMode">
          <enum>QQuickWidget::SizeRootObjectToView</enum>
         </property>
        </widget>
       </item>
       <item>
        <widget class="QLabel" name="label">
         <property name="text">
          <string>Checking for update...</string>
         </property>
        </widget>
       </item>
      </layout>
     </widget>
     <widget class="QWidget" name="errorPage">
      <layout class="QVBoxLayout" name="verticalLayout_4" stretch="0,1,0">
       <item>
        <widget class="QLabel" name="errorTitleLabel">
         <property name="font">
          <font>
           <pointsize>12</pointsize>
          </font>
         </property>
         <property name="text">
          <string notr="true">title placeholder</string>
         </property>
        </widget>
       </item>
       <item>
        <widget class="QLabel" name="errorMessageLabel">
         <property name="text">
          <string notr="true">message placeholder</string>
         </property>
         <property name="alignment">
          <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
         </property>
        </widget>
       </item>
       <item>
        <widget class="QDialogButtonBox" name="errorButtonBox">
         <property name="standardButtons">
          <set>QDialogButtonBox::Close</set>
         </property>
        </widget>
       </item>
      </layout>
     </widget>
     <widget class="QWidget" name="updaterPage">
      <layout class="QVBoxLayout" name="verticalLayout_3">
       <property name="leftMargin">
        <number>0</number>
       </property>
       <property name="topMargin">
        <number>0</number>
       </property>
       <property name="rightMargin">
        <number>0</number>
       </property>
       <property name="bottomMargin">
        <number>0</number>
       </property>
      </layout>
     </widget>
    </widget>
   </item>
  </layout>
 </widget>
 <customwidgets>
  <customwidget>
   <class>QQuickWidget</class>
   <extends>QWidget</extends>
   <header location="global">QtQuickWidgets/QQuickWidget</header>
  </customwidget>
 </customwidgets>
 <resources/>
 <connections/>
</ui>
07070100000047000081a400000000000000000000000168cf694000001a31000000000000000000000000000000000000001700000000src/ui/update_main.cpp// system includes
#include <iostream>
#include <sstream>

// library includes
#include <QApplication>
#include <QCheckBox>
#include <QCommandLineParser>
#include <QFile>
#include <QLibraryInfo>
#include <QMessageBox>
#include <QPushButton>
#include <QTranslator>
#include <QThread>
extern "C" {
    #include <appimage/appimage.h>
}
#include <appimage/update/qt-ui.h>

// local includes
#include "shared.h"
#include "translationmanager.h"
#include "ui_update.h"

using namespace appimage::update::qt;

class UpdateDialog : public QDialog {
    Q_OBJECT

public:
    explicit UpdateDialog(const QString& pathToAppImage) : _pathToAppImage(pathToAppImage), _ui(new Ui::UpdateDialog), _updater(new QtUpdater(pathToAppImage)) {
        // configure UI
        _ui->setupUi(this);

        _ui->stackedWidget->setCurrentIndex(0);

        // these three calls are needed to give the QQuickWidget the same background color as its parent window
        _ui->spinnerQuickWidget->setAttribute(Qt::WA_AlwaysStackOnTop);
        _ui->spinnerQuickWidget->setAttribute(Qt::WA_TranslucentBackground);
        _ui->spinnerQuickWidget->setClearColor(Qt::transparent);

        _ui->spinnerQuickWidget->setSource(QUrl::fromLocalFile(":/update_spinner.qml"));

        // can't add the widget to the page directly in the .ui file since the constructor needs parameters
        _ui->updaterPage->layout()->addWidget(_updater);

        // make sure the QDialog resizes with the spoiler
        layout()->setSizeConstraint(QLayout::SetFixedSize);

        // make sure that when the embedded dialog closes, the parent dialog closes, too
        connect(_updater, &QDialog::finished, this, &QDialog::done);

        // set up updater
        _updater->enableRunUpdatedAppImageButton(false);

        connect(
            _updater,
            &QtUpdater::newStatusMessage,
            this,
            [this](const std::string& newMessage) {
                if (_updaterStatusMessages.tellp() > 0)
                    _updaterStatusMessages << std::endl;

                _updaterStatusMessages << newMessage;
            }
        );

        connect(this, &UpdateDialog::updateCheckFinished, this, [this](int updateCheckResult) {
            switch (updateCheckResult) {
                case 1:
                    clearStatusMessages();
                    _ui->stackedWidget->setCurrentWidget(_ui->updaterPage);
                    _updater->update();
                    return;
                case 0: {
                    _ui->errorTitleLabel->setText(tr("No updates found"));
                    _ui->errorMessageLabel->setText(tr("Could not find updates for AppImage %1").arg(_pathToAppImage));
                    connect(_ui->errorButtonBox, &QDialogButtonBox::clicked, this, &QDialog::accept);
                    break;
                }
                case -1: {
                    _ui->errorTitleLabel->setText(tr("No update information found"));
                    _ui->errorMessageLabel->setText(
                        tr("Could not find update information in AppImage:\n%1"
                           "\n"
                           "\n"
                           "The AppImage doesn't support updating. Please ask the authors to embed "
                           "update information to allow for easy updating."
                        ).arg(_pathToAppImage)
                    );
                    connect(_ui->errorButtonBox, &QDialogButtonBox::clicked, this, &QDialog::reject);
                    break;
                }
                default: {
                    _ui->errorTitleLabel->setText(tr("Update check failed"));
                    _ui->errorMessageLabel->setText(tr("Failed to check for updates:\n%1").arg(_pathToAppImage));
                    connect(_ui->errorButtonBox, &QDialogButtonBox::clicked, this, &QDialog::reject);
                    break;
                }
            }

            _ui->stackedWidget->setCurrentWidget(_ui->errorPage);
            qCritical() << statusMessages();
        });

        asyncCheckForUpdate();
    }

    ~UpdateDialog() override {
        // TODO: parenting in libappimageupdate
        delete _updater;
    }

    QString statusMessages() const {
        return QString::fromStdString(_updaterStatusMessages.str());
    }

    void clearStatusMessages() {
        _updaterStatusMessages.clear();
    }

    void asyncCheckForUpdate() {
        // TODO: implement an async update check in libappimageupdate
        auto* thread = QThread::create([this]() {
            auto updateCheckResult = _updater->checkForUpdates();
            emit updateCheckFinished(updateCheckResult);
        });
        thread->start();
    }

signals:
    void updateCheckFinished(int checkResult);

private:
    const QString _pathToAppImage;
    Ui::UpdateDialog* _ui;
    QtUpdater *_updater;
    std::ostringstream _updaterStatusMessages;
};

#include "update_main.moc"


int main(int argc, char** argv) {
    QCommandLineParser parser;
    parser.setApplicationDescription(QObject::tr("Updates AppImages after desktop integration, for use by Linux distributions"));

    QApplication app(argc, argv);
    QApplication::setApplicationDisplayName(QObject::tr("AppImageLauncher update", "update helper app name"));
    QApplication::setWindowIcon(QIcon(":/AppImageLauncher.svg"));

    std::ostringstream version;
    version << "version " << APPIMAGELAUNCHER_VERSION << " "
            << "(git commit " << APPIMAGELAUNCHER_GIT_COMMIT << "), built on "
            << APPIMAGELAUNCHER_BUILD_DATE;
    QApplication::setApplicationVersion(QString::fromStdString(version.str()));

    // install translations
    TranslationManager translationManager(app);

    parser.addHelpOption();
    parser.addVersionOption();

    parser.process(app);

    parser.addPositionalArgument("path", "Path to AppImage", "<path>");

    if (parser.positionalArguments().empty()) {
        parser.showHelp(1);
    }

    const auto pathToAppImage = parser.positionalArguments().first();

    auto criticalUpdaterError = [](const QString& message) {
        QMessageBox::critical(nullptr, "Error", message);
    };

    if (!QFile(pathToAppImage).exists()) {
        criticalUpdaterError(QString::fromStdString(QObject::tr("Error: no such file or directory: %1").arg(pathToAppImage).toStdString()));
        return 1;
    }

    const auto type = appimage_get_type(pathToAppImage.toStdString().c_str(), false);

    if (type <= 0 || type > 2) {
        criticalUpdaterError(QObject::tr("Not an AppImage: %1").arg(pathToAppImage));
        return 1;
    }

    auto dialog = new UpdateDialog(pathToAppImage);
    dialog->show();

    return QApplication::exec();
}
07070100000048000081a400000000000000000000000168cf6940000000dc000000000000000000000000000000000000001a00000000src/ui/update_spinner.qmlimport QtQuick.Controls 2.15
import QtQuick.Layouts 1.15

ColumnLayout {
    BusyIndicator {
        running: true
        width: 64
        height: 64
        Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
    }
}
07070100000049000041ed00000000000000000000000168cf694000000000000000000000000000000000000000000000000700000000src/ui0707010000004a000041ed00000000000000000000000168cf694000000000000000000000000000000000000000000000000400000000src07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000b00000000TRAILER!!!
openSUSE Build Service is sponsored by