From: Scarlett Moore Date: Sun, 10 Feb 2019 13:08:27 +0000 (+0000) Subject: Import libappimage_0.1.9+dfsg.orig.tar.xz X-Git-Tag: archive/raspbian/1.0.4-5-1+rpi1~1^2^2~3 X-Git-Url: https://dgit.raspbian.org/?a=commitdiff_plain;h=1313a152d4cd1474a0665baf2b2dfe1261378e49;p=libappimage.git Import libappimage_0.1.9+dfsg.orig.tar.xz [dgit import orig libappimage_0.1.9+dfsg.orig.tar.xz] --- 1313a152d4cd1474a0665baf2b2dfe1261378e49 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..25d2462 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*build*/ +cmake-build-*/ +.idea/ +Testing/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..cd6332f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/gtest"] + path = lib/gtest + url = https://github.com/google/googletest.git diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..6618554 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,37 @@ +language: c +compiler: gcc + +services: +- docker + +matrix: + include: + - env: + - ARCH=x86_64 + - DOCKER_IMAGE=quay.io/appimage/appimagebuild + - env: + - ARCH=i686 + - DOCKER_IMAGE=quay.io/appimage/appimagebuild-i386 + - env: + - ARCH=x86_64 + addons: + apt: + update: true + packages: + - libfuse-dev + - desktop-file-utils + +script: +- bash travis/travis-build.sh + +notifications: + irc: + channels: + - "chat.freenode.net#AppImage" + on_success: always # options: [always|never|change] default: always + on_failure: always # options: [always|never|change] default: always + on_start: always # options: [always|never|change] default: always + template: + - "%{repository} build %{build_number}: %{result} %{build_url}" + use_notice: true + # skip_join: true diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..b9de6cb --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.2) + +project(libappimage) + +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_STANDARD_REQUIRED ON) + +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${PROJECT_SOURCE_DIR}/cmake) + +# versioning +set(V_MAJOR 0) +set(V_MINOR 1) +set(V_PATCH 9) + +include(cmake/tools.cmake) +include(cmake/dependencies.cmake) + +# used by e.g., Debian packaging infrastructure +include(GNUInstallDirs) + +add_subdirectory(lib) +add_subdirectory(src) + +include(CTest) +add_subdirectory(tests) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f3777cc --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ +MIT License + +If not stated otherwise within the individual file or subdirectory, the +original source code in this repository is licensed as below. This does not +necessarily apply for all dependencies. For the sake of clarity, this license +does NOT apply to the contents of AppImages that anyone may create. +Software contained inside an AppImage may be licensed under any license at the +discretion of the respecive rights holder(s). + +Copyright (c) 2004-19 Simon Peter and the AppImage team + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1643394 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# libappimage diff --git a/cmake/Modules/Findsquashfuse.cmake b/cmake/Modules/Findsquashfuse.cmake new file mode 100644 index 0000000..24529a4 --- /dev/null +++ b/cmake/Modules/Findsquashfuse.cmake @@ -0,0 +1,18 @@ +cmake_minimum_required(VERSION 3.0) + +find_path(squashfuse_H_DIR + NAMES squashfuse.h + HINTS ${CMAKE_INSTALL_PREFIX} + PATH_SUFFIXES include include/linux +) + +if(NOT squashfuse_H_DIR) + message(FATAL_ERROR "squashfuse.h not found") +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(squashfuse DEFAULT_MSG SQUASHFUSE_INCLUDE_DIR SQUASHFUSE_LIBRARY_DIR) + +add_library(squashfuse IMPORTED SHARED) +set_property(TARGET squashfuse PROPERTY IMPORTED_LOCATION ${squashfuse_LIBRARY}) +set_property(TARGET squashfuse PROPERTY INTERFACE_INCLUDE_DIRECTORIES "${squashfuse_H_DIR}") diff --git a/cmake/dependencies.cmake b/cmake/dependencies.cmake new file mode 100644 index 0000000..8d96484 --- /dev/null +++ b/cmake/dependencies.cmake @@ -0,0 +1,133 @@ +# >= 3.2 required for ExternalProject_Add_StepDependencies +cmake_minimum_required(VERSION 3.2) + +include(${CMAKE_CURRENT_LIST_DIR}/scripts.cmake) + +# imported dependencies +include(${CMAKE_CURRENT_LIST_DIR}/imported_dependencies.cmake) + +if(USE_CCACHE) + message(STATUS "Using CCache to build AppImageKit dependencies") + # TODO: find way to use find_program with all possible paths + # (might differ from distro to distro) + # these work on Debian and Ubuntu: + set(CC "/usr/lib/ccache/gcc") + set(CXX "/usr/lib/ccache/g++") +else() + set(CC "${CMAKE_C_COMPILER}") + set(CXX "${CMAKE_CXX_COMPILER}") +endif() + +set(CFLAGS ${DEPENDENCIES_CFLAGS}) +set(CPPFLAGS ${DEPENDENCIES_CPPFLAGS}) +set(LDFLAGS ${DEPENDENCIES_LDFLAGS}) + + +set(USE_SYSTEM_XZ OFF CACHE BOOL "Use system xz/liblzma instead of building our own") + +if(NOT USE_SYSTEM_XZ) + message(STATUS "Downloading and building xz") + + ExternalProject_Add(xz-EXTERNAL + URL https://netcologne.dl.sourceforge.net/project/lzmautils/xz-5.2.3.tar.gz + URL_HASH SHA512=a5eb4f707cf31579d166a6f95dbac45cf7ea181036d1632b4f123a4072f502f8d57cd6e7d0588f0bf831a07b8fc4065d26589a25c399b95ddcf5f73435163da6 + CONFIGURE_COMMAND CC=${CC} CXX=${CXX} CFLAGS=${CFLAGS} CPPFLAGS=${CPPFLAGS} LDFLAGS=${LDFLAGS} /configure --with-pic --disable-shared --enable-static --prefix= --libdir=/lib ${EXTRA_CONFIGURE_FLAGS} --disable-xz --disable-xzdec + BUILD_COMMAND ${MAKE} + INSTALL_COMMAND ${MAKE} install + ) + + import_external_project( + TARGET_NAME xz + EXT_PROJECT_NAME xz-EXTERNAL + LIBRARY_DIRS /lib/ + LIBRARIES "/lib/liblzma.a" + INCLUDE_DIRS "/src/liblzma/api/" + ) +else() + message(STATUS "Using system xz") + + import_pkgconfig_target(TARGET_NAME xz PKGCONFIG_TARGET liblzma) +endif() + + +# as distros don't provide suitable squashfuse and squashfs-tools, those dependencies are bundled in, can, and should +# be used from this repository for AppImageKit +# for distro packaging, it can be linked to an existing package just fine +set(USE_SYSTEM_SQUASHFUSE OFF CACHE BOOL "Use system libsquashfuse instead of building our own") + +if(NOT USE_SYSTEM_SQUASHFUSE) + message(STATUS "Downloading and building squashfuse") + + # TODO: implement out-of-source builds for squashfuse, as for the other dependencies + configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/src/patches/patch-squashfuse.sh.in + ${CMAKE_CURRENT_BINARY_DIR}/patch-squashfuse.sh + @ONLY + ) + + ExternalProject_Add(squashfuse-EXTERNAL + GIT_REPOSITORY https://github.com/vasi/squashfuse/ + GIT_TAG 1f98030 + UPDATE_COMMAND "" # make sure CMake won't try to fetch updates unnecessarily and hence rebuild the dependency every time + PATCH_COMMAND bash -xe ${CMAKE_CURRENT_BINARY_DIR}/patch-squashfuse.sh + CONFIGURE_COMMAND ${LIBTOOLIZE} --force + COMMAND env ACLOCAL_FLAGS="-I /usr/share/aclocal" aclocal + COMMAND ${AUTOHEADER} + COMMAND ${AUTOMAKE} --force-missing --add-missing + COMMAND ${AUTORECONF} -fi || true + COMMAND ${SED} -i "/PKG_CHECK_MODULES.*/,/,:./d" configure # https://github.com/vasi/squashfuse/issues/12 + COMMAND ${SED} -i "s/typedef off_t sqfs_off_t/typedef int64_t sqfs_off_t/g" common.h # off_t's size might differ, see https://stackoverflow.com/a/9073762 + COMMAND CC=${CC} CXX=${CXX} CFLAGS=${CFLAGS} LDFLAGS=${LDFLAGS} /configure --disable-demo --disable-high-level --without-lzo --without-lz4 --prefix= --libdir=/lib --with-xz=${xz_PREFIX} ${EXTRA_CONFIGURE_FLAGS} + COMMAND ${SED} -i "s|XZ_LIBS = -llzma |XZ_LIBS = -Bstatic ${xz_LIBRARIES}/|g" Makefile + BUILD_COMMAND ${MAKE} + BUILD_IN_SOURCE ON + INSTALL_COMMAND ${MAKE} install + ) + + import_external_project( + TARGET_NAME libsquashfuse + EXT_PROJECT_NAME squashfuse-EXTERNAL + LIBRARIES "/.libs/libsquashfuse.a;/.libs/libsquashfuse_ll.a;/.libs/libfuseprivate.a" + INCLUDE_DIRS "" + ) +else() + message(STATUS "Using system squashfuse") + + import_pkgconfig_target(TARGET_NAME libsquashfuse PKGCONFIG_TARGET squashfuse) +endif() + + +set(USE_SYSTEM_LIBARCHIVE OFF CACHE BOOL "Use system libarchive instead of building our own") + +if(NOT USE_SYSTEM_LIBARCHIVE) + message(STATUS "Downloading and building libarchive") + + ExternalProject_Add(libarchive-EXTERNAL + URL https://www.libarchive.org/downloads/libarchive-3.3.1.tar.gz + URL_HASH SHA512=90702b393b6f0943f42438e277b257af45eee4fa82420431f6a4f5f48bb846f2a72c8ff084dc3ee9c87bdf8b57f4d8dddf7814870fe2604fe86c55d8d744c164 + CONFIGURE_COMMAND CC=${CC} CXX=${CXX} CFLAGS=${CFLAGS} CPPFLAGS=${CPPFLAGS} LDFLAGS=${LDFLAGS} /configure --with-pic --disable-shared --enable-static --disable-bsdtar --disable-bsdcat --disable-bsdcpio --with-zlib --without-bz2lib --without-iconv --without-lz4 --without-lzma --without-lzo2 --without-nettle --without-openssl --without-xml2 --without-expat --prefix= --libdir=/lib ${EXTRA_CONFIGURE_FLAGS} + BUILD_COMMAND ${MAKE} + INSTALL_COMMAND ${MAKE} install + ) + + import_external_project( + TARGET_NAME libarchive + EXT_PROJECT_NAME libarchive-EXTERNAL + LIBRARIES "/lib/libarchive.a" + INCLUDE_DIRS "/include/" + ) +else() + message(STATUS "Using system libarchive") + + import_find_pkg_target(libarchive LibArchive LibArchive) +endif() + + +#### build dependency configuration #### + +# only have to build custom xz when not using system libxz +if(TARGET xz-EXTERNAL) + if(TARGET squashfuse-EXTERNAL) + ExternalProject_Add_StepDependencies(squashfuse-EXTERNAL configure xz-EXTERNAL) + endif() +endif() diff --git a/cmake/imported_dependencies.cmake b/cmake/imported_dependencies.cmake new file mode 100644 index 0000000..56d7fc0 --- /dev/null +++ b/cmake/imported_dependencies.cmake @@ -0,0 +1,10 @@ +include(${CMAKE_CURRENT_LIST_DIR}/scripts.cmake) + +# the names of the targets need to differ from the library filenames +# this is especially an issue with libcairo, where the library is called libcairo +# therefore, all libs imported this way have been prefixed with lib +import_pkgconfig_target(TARGET_NAME libglib PKGCONFIG_TARGET glib-2.0>=2.40) +import_pkgconfig_target(TARGET_NAME libgobject PKGCONFIG_TARGET gobject-2.0>=2.40) +import_pkgconfig_target(TARGET_NAME libgio PKGCONFIG_TARGET gio-2.0>=2.40) +import_pkgconfig_target(TARGET_NAME libzlib PKGCONFIG_TARGET zlib) +import_pkgconfig_target(TARGET_NAME libcairo PKGCONFIG_TARGET cairo) diff --git a/cmake/libappimageConfig.cmake.in b/cmake/libappimageConfig.cmake.in new file mode 100644 index 0000000..29b8238 --- /dev/null +++ b/cmake/libappimageConfig.cmake.in @@ -0,0 +1,22 @@ +# - Config file for the AppImage package +# Exported Targets +# - libappimage +# - libappimage_shared +# +# Exported Variables (DEPRECATED use the exported targets instead) +# LIBAPPIMAGE_INCLUDE_DIRS - include directories for LIBAPPIMAGE +# LIBAPPIMAGE_LIBRARIES - libraries to link against + +@PACKAGE_INIT@ + +# Compute paths +get_filename_component(LIBAPPIMAGE_CMAKE_DIR ${CMAKE_CURRENT_LIST_FILE} PATH) + +# Import dependencies implicitly required by libappimageTargets.cmake +include(${LIBAPPIMAGE_CMAKE_DIR}/imported_dependencies.cmake) + +# Our library dependencies (contains definitions for IMPORTED targets) +include(${LIBAPPIMAGE_CMAKE_DIR}/libappimageTargets.cmake) + +get_target_property(LIBAPPIMAGE_INCLUDE_DIRS libappimage INTERFACE_INCLUDE_DIRECTORIES) +set(LIBAPPIMAGE_LIBRARIES libappimage) diff --git a/cmake/libappimageConfigVersion.cmake.in b/cmake/libappimageConfigVersion.cmake.in new file mode 100644 index 0000000..a55ea68 --- /dev/null +++ b/cmake/libappimageConfigVersion.cmake.in @@ -0,0 +1,11 @@ +set(PACKAGE_VERSION "@GIT_COMMIT@") + +# Check whether the requested PACKAGE_FIND_VERSION is compatible +if("${PACKAGE_VERSION}" VERSION_LESS "${PACKAGE_FIND_VERSION}") + set(PACKAGE_VERSION_COMPATIBLE FALSE) +else() + set(PACKAGE_VERSION_COMPATIBLE TRUE) + if ("${PACKAGE_VERSION}" VERSION_EQUAL "${PACKAGE_FIND_VERSION}") + set(PACKAGE_VERSION_EXACT TRUE) + endif() +endif() diff --git a/cmake/scripts.cmake b/cmake/scripts.cmake new file mode 100644 index 0000000..6362314 --- /dev/null +++ b/cmake/scripts.cmake @@ -0,0 +1,229 @@ +cmake_minimum_required(VERSION 3.2) + +include(ExternalProject) + +# searches for absolute paths of libraries, applying LIBRARY_DIRS +# CMake prefers absolute paths of libraries in non-standard locations, apparently +# see FindPkgConfig.cmake's _pkg_find_libs for more information +# +# positional parameters: +# - libraries: name of variable containing list of libraries +# - library_dirs +function(apply_library_dirs libraries library_dirs) + foreach(library ${${libraries}}) + find_library(${library}_path ${library} PATHS ${${library_dirs}} NO_DEFAULT_PATH) + if(NOT ${library}_path) + list(APPEND new_libraries ${library}) + else() + list(APPEND new_libraries ${${library}_path}) + endif() + endforeach() + set(${libraries} ${new_libraries} PARENT_SCOPE) + unset(new_libraries) +endfunction() + +# imports a library from the standard set of variables CMake creates when using its pkg-config module or find_package +# this is code shared by import_pkgconfig_target and import_external_project, hence it's been extracted into a separate +# CMake function +# +# partly inspired by https://github.com/Kitware/CMake/blob/master/Modules/FindPkgConfig.cmake#L187 +# +# positional parameters: +# - target_name: name of the target that should be created +# - variable_prefix: prefix of the variable that should be used to create the target from +function(import_library_from_prefix target_name variable_prefix) + if(TARGET ${target_name}) + message(WARNING "Target exists already, skipping") + return() + endif() + + add_library(${target_name} INTERFACE IMPORTED GLOBAL) + + if(${variable_prefix}_INCLUDE_DIRS) + # need to create directories before setting INTERFACE_INCLUDE_DIRECTORIES, otherwise CMake will complain + # possibly related: https://cmake.org/Bug/view.php?id=15052 + foreach(dir ${${variable_prefix}_INCLUDE_DIRS}) + if(NOT EXISTS ${dir}) + if (${dir} MATCHES ${CMAKE_BINARY_DIR}) + file(MAKE_DIRECTORY ${dir}) + list(APPEND include_dirs ${dir}) + endif() + else() + list(APPEND include_dirs ${dir}) + endif() + endforeach() + set_property(TARGET ${target_name} PROPERTY INTERFACE_INCLUDE_DIRECTORIES ${include_dirs}) + unset(include_dirs) + endif() + + # if library dirs are set, figure out absolute paths to libraries, like CMake's FindPkgConfig module does + if(${variable_prefix}_LIBRARY_DIRS) + apply_library_dirs(${variable_prefix}_LIBRARIES ${variable_prefix}_LIBRARY_DIRS) + endif() + + set_property(TARGET ${target_name} PROPERTY INTERFACE_LINK_LIBRARIES ${${variable_prefix}_LIBRARIES}) + + if(${variable_prefix}_CFLAGS_OTHER) + set_property(TARGET ${target_name} PROPERTY INTERFACE_COMPILE_OPTIONS ${${variable_prefix}_CFLAGS_OTHER}) + endif() + + # export some of the imported properties with the target name as prefix + # this is necessary to allow the other external projects, which are not built with CMake or not within the same + # CMake context, to link to the libraries built as external projects (or the system ones, depending on the build + # configuration) + set(${target_name}_INCLUDE_DIRS ${${variable_prefix}_INCLUDE_DIRS} CACHE INTERNAL "") + set(${target_name}_LIBRARIES ${${variable_prefix}_LIBRARIES} CACHE INTERNAL "") + set(${target_name}_LIBRARY_DIRS ${${variable_prefix}_LIBRARY_DIRS} CACHE INTERNAL "") + # TODO: the following might not always apply + set(${target_name}_PREFIX ${CMAKE_INSTALL_PREFIX}/lib CACHE INTERNAL "") +endfunction() + + +# imports a library using pkg-config +# +# positional parameters: +# - target_name: name of the target that we shall create for you +# - pkg_config_target: librar(y name to pass to pkg-config (may include a version) +function(import_pkgconfig_target) + set(keywords STATIC OPTIONAL) + set(oneValueArgs TARGET_NAME PKGCONFIG_TARGET) + cmake_parse_arguments(IMPORT_PKGCONFIG_TARGET "${keywords}" "${oneValueArgs}" "" "${ARGN}") + + # check whether parameters have been set + if(NOT IMPORT_PKGCONFIG_TARGET_TARGET_NAME) + message(FATAL_ERROR "TARGET_NAME parameter missing, but is required") + endif() + if(NOT IMPORT_PKGCONFIG_TARGET_PKGCONFIG_TARGET) + message(FATAL_ERROR "PKGCONFIG_TARGET parameter missing, but is required") + endif() + + find_package(PkgConfig REQUIRED) + + set(type "shared") + if(IMPORT_PKGCONFIG_TARGET_STATIC) + set(type "static") + endif() + + message(STATUS "Importing target ${IMPORT_PKGCONFIG_TARGET_TARGET_NAME} via pkg-config (${IMPORT_PKGCONFIG_TARGET_PKGCONFIG_TARGET}, ${type})") + + if(NOT IMPORT_PKGCONFIG_TARGET_OPTIONAL) + set(extra_args REQUIRED) + endif() + + pkg_check_modules(${IMPORT_PKGCONFIG_TARGET_TARGET_NAME}-IMPORTED ${IMPORT_PKGCONFIG_TARGET_PKGCONFIG_TARGET} ${extra_args}) + + if(NOT ${IMPORT_PKGCONFIG_TARGET_TARGET_NAME}-IMPORTED_FOUND AND IMPORT_PKGCONFIG_TARGET_OPTIONAL) + return() + endif() + + if(IMPORT_PKGCONFIG_TARGET_STATIC) + set(prefix ${IMPORT_PKGCONFIG_TARGET_TARGET_NAME}-IMPORTED_STATIC) + else() + set(prefix ${IMPORT_PKGCONFIG_TARGET_TARGET_NAME}-IMPORTED) + endif() + + import_library_from_prefix(${IMPORT_PKGCONFIG_TARGET_TARGET_NAME} ${prefix}) +endfunction() + +function(import_find_pkg_target target_name pkg_name variable_prefix) + message(STATUS "Importing target ${target_name} via find_package (${pkg_name})") + + find_package(${pkg_name}) + if(NOT ${variable_prefix}_FOUND) + message(FATAL_ERROR "${pkg_name} could not be found on the system. You will have to either install it, or use the bundled package.") + endif() + + import_library_from_prefix(${target_name} ${variable_prefix}) +endfunction() + + +# imports a library from an existing external project +# +# required parameters: +# - TARGET_NAME: +function(import_external_project) + set(oneValueArgs TARGET_NAME EXT_PROJECT_NAME) + set(multiValueArgs LIBRARIES INCLUDE_DIRS LIBRARY_DIRS) + cmake_parse_arguments(IMPORT_EXTERNAL_PROJECT "" "${oneValueArgs}" "${multiValueArgs}" "${ARGN}") + + # check whether parameters have been set + if(NOT IMPORT_EXTERNAL_PROJECT_TARGET_NAME) + message(FATAL_ERROR "TARGET_NAME parameter missing, but is required") + endif() + if(NOT IMPORT_EXTERNAL_PROJECT_EXT_PROJECT_NAME) + message(FATAL_ERROR "EXT_PROJECT_NAME parameter missing, but is required") + endif() + if(NOT IMPORT_EXTERNAL_PROJECT_LIBRARIES) + message(FATAL_ERROR "LIBRARIES parameter missing, but is required") + endif() + if(NOT IMPORT_EXTERNAL_PROJECT_INCLUDE_DIRS) + message(FATAL_ERROR "INCLUDE_DIRS parameter missing, but is required") + endif() + + if(TARGET ${target_name}) + message(WARNING "Target exists already, skipping") + return() + endif() + + add_library(${IMPORT_EXTERNAL_PROJECT_TARGET_NAME} INTERFACE IMPORTED GLOBAL) + + ExternalProject_Get_Property(${IMPORT_EXTERNAL_PROJECT_EXT_PROJECT_NAME} SOURCE_DIR) + ExternalProject_Get_Property(${IMPORT_EXTERNAL_PROJECT_EXT_PROJECT_NAME} INSTALL_DIR) + + # "evaluate" patterns in the passed arguments by using some string replacing magic + # this makes it easier to use this function, as some external project properties don't need to be evaluated and + # passed beforehand, and should reduce the amount of duplicate code in this file + foreach(item ITEMS + IMPORT_EXTERNAL_PROJECT_EXT_PROJECT_NAME + IMPORT_EXTERNAL_PROJECT_LIBRARIES + IMPORT_EXTERNAL_PROJECT_INCLUDE_DIRS + IMPORT_EXTERNAL_PROJECT_LIBRARY_DIRS) + + # create new variable with fixed string... + string(REPLACE "" "${SOURCE_DIR}" ${item}-out "${${item}}") + # ... and set the original value to the new value + set(${item} "${${item}-out}") + + # create new variable with fixed string... + string(REPLACE "" "${INSTALL_DIR}" ${item}-out "${${item}}") + # ... and set the original value to the new value + set(${item} "${${item}-out}") + endforeach() + + # if library dirs are set, figure out absolute paths to libraries, like CMake's FindPkgConfig module does + if(${IMPORT_EXTERNAL_PROJECT_LIBRARY_DIRS}) + apply_library_dirs(IMPORT_EXTERNAL_PROJECT_LIBRARIES IMPORT_EXTERNAL_PROJECT_LIBRARY_DIRS) + endif() + + set_property(TARGET ${IMPORT_EXTERNAL_PROJECT_TARGET_NAME} PROPERTY INTERFACE_LINK_LIBRARIES "${IMPORT_EXTERNAL_PROJECT_LIBRARIES}") + + if(IMPORT_EXTERNAL_PROJECT_INCLUDE_DIRS) + # need to create directories before setting INTERFACE_INCLUDE_DIRECTORIES, otherwise CMake will complain + # possibly related: https://cmake.org/Bug/view.php?id=15052 + + foreach(dir ${IMPORT_EXTERNAL_PROJECT_INCLUDE_DIRS}) + if(NOT EXISTS ${dir}) + if (${dir} MATCHES ${CMAKE_BINARY_DIR}) + file(MAKE_DIRECTORY ${dir}) + list(APPEND include_dirs ${dir}) + endif() + else() + list(APPEND include_dirs ${dir}) + endif() + endforeach() + set_property(TARGET ${IMPORT_EXTERNAL_PROJECT_TARGET_NAME} PROPERTY INTERFACE_INCLUDE_DIRECTORIES ${include_dirs}) + unset(include_dirs) + endif() + + # finally, add a depenceny on the external project to make sure it's built + add_dependencies(${IMPORT_EXTERNAL_PROJECT_TARGET_NAME} "${IMPORT_EXTERNAL_PROJECT_EXT_PROJECT_NAME}") + + # read external project properties, and export them with the target name as prefix + # this is necessary to allow the other external projects, which are not built with CMake or not within the same + # CMake context, to link to the libraries built as external projects (or the system ones, depending on the build + # configuration) + set(${IMPORT_EXTERNAL_PROJECT_TARGET_NAME}_INCLUDE_DIRS "${IMPORT_EXTERNAL_PROJECT_INCLUDE_DIRS}" CACHE INTERNAL "") + set(${IMPORT_EXTERNAL_PROJECT_TARGET_NAME}_LIBRARIES "${IMPORT_EXTERNAL_PROJECT_LIBRARIES}" CACHE INTERNAL "") + set(${IMPORT_EXTERNAL_PROJECT_TARGET_NAME}_LIBRARY_DIRS "${IMPORT_EXTERNAL_PROJECT_LIBRARY_DIRS}" CACHE INTERNAL "") + set(${IMPORT_EXTERNAL_PROJECT_TARGET_NAME}_PREFIX ${INSTALL_DIR} CACHE INTERNAL "") +endfunction() diff --git a/cmake/tools.cmake b/cmake/tools.cmake new file mode 100644 index 0000000..d0941bb --- /dev/null +++ b/cmake/tools.cmake @@ -0,0 +1,61 @@ +if(TOOLS_PREFIX) + message(STATUS "TOOLS_PREFIX set, looking for programs with prefix ${TOOLS_PREFIX}") +endif() + +# first of all, make sure required programs are available +function(check_program) + set(keywords FORCE_PREFIX) + set(oneValueArgs NAME) + cmake_parse_arguments(ARG "${keywords}" "${oneValueArgs}" "" "${ARGN}") + + if(NOT ARG_NAME) + message(FATAL_ERROR "NAME argument required for check_program") + endif() + + if(TOOLS_PREFIX) + set(prefix ${TOOLS_PREFIX}) + endif() + + message(STATUS "Checking for program ${ARG_NAME}") + + string(TOUPPER ${ARG_NAME} name_upper) + + if(prefix) + # try prefixed version first + find_program(${name_upper} ${prefix}${ARG_NAME}) + endif() + + # try non-prefixed version + if(NOT ${name_upper}) + if(TOOLS_PREFIX AND ARG_FORCE_PREFIX) + message(FATAL_ERROR "TOOLS_PREFIX set, but could not find program with prefix in PATH (FORCE_PREFIX is set)") + endif() + + find_program(${name_upper} ${ARG_NAME}) + + if(NOT ${name_upper}) + message(FATAL_ERROR "Could not find required program ${ARG_NAME}.") + endif() + endif() + + message(STATUS "Found program ${ARG_NAME}: ${${name_upper}}") + + mark_as_advanced(${name_upper}) +endfunction() + +check_program(NAME aclocal) +check_program(NAME autoheader) +check_program(NAME automake) +check_program(NAME autoreconf) +check_program(NAME libtoolize) +check_program(NAME patch) +check_program(NAME sed) +check_program(NAME wget) +check_program(NAME xxd) +check_program(NAME desktop-file-validate) +check_program(NAME objcopy FORCE_PREFIX) +check_program(NAME objdump FORCE_PREFIX) +check_program(NAME readelf FORCE_PREFIX) +check_program(NAME strip FORCE_PREFIX) +check_program(NAME make) +# TODO: add checks for remaining commands diff --git a/include/appimage/appimage.h b/include/appimage/appimage.h new file mode 100644 index 0000000..7262d53 --- /dev/null +++ b/include/appimage/appimage.h @@ -0,0 +1,137 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +// include header of shared library, which contains more appimage_ functions +#include + +/* Return the md5 hash constructed according to +* https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html#THUMBSAVE +* This can be used to identify files that are related to a given AppImage at a given location */ +char *appimage_get_md5(char const* path); + +/* Check if a file is an AppImage. Returns the image type if it is, or -1 if it isn't */ +int appimage_get_type(const char* path, bool verbose); + +/* + * Finds the desktop file of a registered AppImage and returns the path + * Returns NULL if the desktop file can't be found, which should only occur when the AppImage hasn't been registered yet + */ +char* appimage_registered_desktop_file_path(const char* path, char* md5, bool verbose); + +/* + * Check whether an AppImage has been registered in the system + */ +bool appimage_is_registered_in_system(const char* path); + +/* Register a type 1 AppImage in the system + * DEPRECATED don't use in newly written code. Use appimage_is_registered_in_system instead. + * */ +bool appimage_type1_register_in_system(const char *path, bool verbose); + +/* Register a type 2 AppImage in the system + * DEPRECATED don't use in newly written code. Use appimage_is_registered_in_system instead. + * */ +bool appimage_type2_register_in_system(const char *path, bool verbose); + +/* + * Register an AppImage in the system + * Returns 0 on success, non-0 otherwise. + */ +int appimage_register_in_system(const char *path, bool verbose); + +/* Unregister an AppImage in the system */ +int appimage_unregister_in_system(const char *path, bool verbose); + +/* Extract a given file from the appimage following the symlinks until a concrete file is found */ +void appimage_extract_file_following_symlinks(const char* appimage_file_path, const char* file_path, const char* target_file_path); + +/* Read a given file from the AppImage into a freshly allocated buffer following symlinks + * Buffer must be free()d after usage + * */ +bool appimage_read_file_into_buffer_following_symlinks(const char* appimage_file_path, const char* file_path, char** buffer, unsigned long* buf_size); + +/* Create AppImage thumbnail according to + * https://specifications.freedesktop.org/thumbnail-spec/0.8.0/index.html + */ +void appimage_create_thumbnail(const char* appimage_file_path, bool verbose); + +/* List files contained in the AppImage file. + * Returns: a newly allocated char** ended at NULL. If no files ware found also is returned a {NULL} + * + * You should ALWAYS take care of releasing this using `appimage_string_list_free`. + * */ +char** appimage_list_files(const char* path); + +/* Releases memory of a string list (a.k.a. list of pointers to char arrays allocated in heap memory). */ +void appimage_string_list_free(char** list); + +/* + * Checks whether a type 1 AppImage's desktop file has set Terminal=true. + * + * Returns >0 if set, 0 if not set, <0 on errors. + */ +int appimage_type1_is_terminal_app(const char* path); + +/* + * Checks whether a type 2 AppImage's desktop file has set Terminal=true. + * + * Returns >0 if set, 0 if not set, <0 on errors. + */ +int appimage_type2_is_terminal_app(const char* path); + +/* + * Checks whether an AppImage's desktop file has set Terminal=true. + * + * Returns >0 if set, 0 if not set, <0 on errors. + */ +int appimage_is_terminal_app(const char* path); + +/* + * Checks whether a type 1 AppImage's desktop file has set X-AppImage-Version=false. + * Useful to check whether the author of an AppImage doesn't want it to be integrated. + * + * Returns >0 if set, 0 if not set, <0 on errors. + */ +int appimage_type1_shall_not_be_integrated(const char* path); + +/* + * Checks whether a type 2 AppImage's desktop file has set X-AppImage-Version=false. + * Useful to check whether the author of an AppImage doesn't want it to be integrated. + * + * Returns >0 if set, 0 if not set, <0 on errors. + */ +int appimage_type2_shall_not_be_integrated(const char* path); + +/* + * Checks whether an AppImage's desktop file has set X-AppImage-Version=false. + * Useful to check whether the author of an AppImage doesn't want it to be integrated. + * + * Returns >0 if set, 0 if not set, <0 on errors. + */ +int appimage_shall_not_be_integrated(const char* path); + +/* + * Calculate the size of an ELF file on disk based on the information in its header + * + * Example: + * + * ls -l 126584 + * + * Calculation using the values also reported by readelf -h: + * Start of section headers e_shoff 124728 + * Size of section headers e_shentsize 64 + * Number of section headers e_shnum 29 + * + * e_shoff + ( e_shentsize * e_shnum ) = 126584 + */ +ssize_t appimage_get_elf_size(const char* fname); + +#ifdef __cplusplus +} +#endif diff --git a/include/appimage/appimage_shared.h b/include/appimage/appimage_shared.h new file mode 100644 index 0000000..88db437 --- /dev/null +++ b/include/appimage/appimage_shared.h @@ -0,0 +1,43 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/* + * Return the offset, and the length of an ELF section with a given name in a given ELF file + */ +bool appimage_get_elf_section_offset_and_length(const char* fname, const char* section_name, unsigned long* offset, unsigned long* length); + +int appimage_print_hex(char* fname, unsigned long offset, unsigned long length); +int appimage_print_binary(char* fname, unsigned long offset, unsigned long length); + +/* + * Creates hexadecimal representation of a byte array. Allocates a new char array (string) with the correct size that + * needs to be free()d. + */ +char* appimage_hexlify(const char* bytes, size_t numBytes); + +/* + * Calculate MD5 digest of AppImage file, skipping the signature and digest sections. + * + * The digest section must be skipped as the value calculated by this method is going to be embedded in it by default. + * + * The signature section must be skipped as the signature will not be available at the time this hash is calculated. + * + * The hash is _not_ compatible with tools like md5sum. + * + * You need to allocate a char array of at least 16 bytes (128 bit) and pass a reference to it as digest parameter. + * The function will set it to the raw digest, without any kind of termination. Please use appimage_hexlify() if you + * need a textual representation. + * + * Please beware that this calculation is only available for type 2 AppImages. + */ +bool appimage_type2_digest_md5(const char* fname, char* digest); + +#ifdef __cplusplus +} +#endif diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt new file mode 100644 index 0000000..4daa663 --- /dev/null +++ b/lib/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.0) + +include(CTest) + +if(BUILD_TESTING) + if(NOT TARGET gtest) + if(IS_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/gtest) + add_subdirectory(gtest EXCLUDE_FROM_ALL) + else() + message(FATAL_ERROR "gtest submodule not found; please call git submodule update --init or disable the unit tests using -DBUILD_TESTING=OFF") + endif() + endif() +endif() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..664bf2a --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,50 @@ +# required for pkg-config to create PkgConfig:: imported library targets +cmake_minimum_required(VERSION 3.6) + +find_package(PkgConfig) + +pkg_check_modules(glib glib-2.0>=2.40 IMPORTED_TARGET) +pkg_check_modules(gobject gobject-2.0>=2.40 IMPORTED_TARGET) +pkg_check_modules(gio gio-2.0>=2.40 IMPORTED_TARGET) +pkg_check_modules(zlib zlib IMPORTED_TARGET) +pkg_check_modules(cairo cairo IMPORTED_TARGET) + +add_subdirectory(xdg-basedir) +add_subdirectory(libappimage_hashlib) +add_subdirectory(libappimage_shared) +add_subdirectory(libappimage) + +# Export the package for use from the build-tree +# (this registers the build-tree with a global CMake-registry) +export(PACKAGE libappimage) + +include(CMakePackageConfigHelpers) + +# Create the AppImageConfig.cmake and AppImageConfigVersion files +configure_package_config_file( + "${PROJECT_SOURCE_DIR}/cmake/libappimageConfig.cmake.in" + "${PROJECT_BINARY_DIR}/${CMAKE_FILES_DIRECTORY}/libappimageConfig.cmake" + INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/libappimage +) +# ... for both +configure_file( + "${PROJECT_SOURCE_DIR}/cmake/libappimageConfigVersion.cmake.in" + "${PROJECT_BINARY_DIR}/${CMAKE_FILES_DIRECTORY}/libappimageConfigVersion.cmake" + @ONLY +) + +# Install the AppImageConfig.cmake and AppImageConfigVersion.cmake +install(FILES + "${PROJECT_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/libappimageConfig.cmake" + "${PROJECT_BINARY_DIR}/${CMAKE_FILES_DIRECTORY}/libappimageConfigVersion.cmake" + "${PROJECT_SOURCE_DIR}/cmake/scripts.cmake" + "${PROJECT_SOURCE_DIR}/cmake/imported_dependencies.cmake" + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/libappimage + COMPONENT libappimage-dev +) + +# Install the export set for use with the install-tree +install(EXPORT libappimageTargets + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/libappimage + COMPONENT libappimage-dev +) diff --git a/src/libappimage/CMakeLists.txt b/src/libappimage/CMakeLists.txt new file mode 100644 index 0000000..64bd098 --- /dev/null +++ b/src/libappimage/CMakeLists.txt @@ -0,0 +1,73 @@ +cmake_minimum_required(VERSION 3.2) + +set(libappimage_public_header ${PROJECT_SOURCE_DIR}/include/appimage/appimage.h) + +# both libraries use the same set of source files +set(libappimage_sources + ${libappimage_public_header} + libappimage.c + appimage_handler.h + appimage_handler.c + type1.c + type2.c + desktop_integration.h + desktop_integration.c +) + +add_library(libappimage SHARED ${libappimage_sources} appimage_handler.h) +add_library(libappimage_static STATIC ${libappimage_sources}) + +# set common options +foreach(target libappimage libappimage_static) + # targets are called lib* already, so CMake shouldn't add another lib prefix to the actual files + set_target_properties(${target} PROPERTIES PREFIX "") + + target_compile_definitions(${target} + # Support Large Files + PRIVATE -D_FILE_OFFSET_BITS=64 + PRIVATE -D_LARGEFILE_SOURCE + + PRIVATE -DGIT_COMMIT="${GIT_COMMIT}" + PRIVATE -DENABLE_BINRELOC + ) + + target_include_directories(${target} + PUBLIC $ + INTERFACE $ + ) + + target_link_libraries(${target} + PRIVATE libarchive + PRIVATE xdg-basedir + # not linking publicly to squashfuse as headers are not needed when using libappimage + # unit tests etc., which use squashfuse directly, must link to it explicitly + PRIVATE libsquashfuse + PRIVATE xz + PUBLIC libappimage_shared + PUBLIC pthread + PUBLIC libglib + PUBLIC libgobject + PUBLIC libgio + PUBLIC libzlib + PUBLIC libcairo + ) + + set_property(TARGET libappimage PROPERTY PUBLIC_HEADER ${libappimage_public_header}) + + set_property(TARGET libappimage PROPERTY VERSION ${V_MAJOR}.${V_MINOR}.${V_PATCH}) + set_property(TARGET libappimage PROPERTY SOVERSION ${V_MAJOR}) +endforeach() + +# install libappimage +install(TARGETS libappimage + EXPORT libappimageTargets + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT libappimage + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT libappimage + PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/appimage COMPONENT libappimage-dev +) + +# Add all targets to the build-tree export set +export( + TARGETS libappimage libappimage_shared libappimage_hashlib + FILE "${PROJECT_BINARY_DIR}/${CMAKE_FILES_DIRECTORY}/libappimageTargets.cmake" +) diff --git a/src/libappimage/appimage_handler.c b/src/libappimage/appimage_handler.c new file mode 100644 index 0000000..8ce607d --- /dev/null +++ b/src/libappimage/appimage_handler.c @@ -0,0 +1,83 @@ +// system includes +#include "stdio.h" + +// library includes +#include + +// local includes +#include "appimage_handler.h" +#include "type1.h" +#include "type2.h" + +/* Factory function for creating the right appimage handler for + * a given file. */ +appimage_handler create_appimage_handler(const char *const path) { + int appimage_type = appimage_get_type(path, 0); + + appimage_handler handler; +#ifdef STANDALONE + fprintf(stderr,"AppImage type: %d\n", appimage_type); +#endif + switch (appimage_type) { + case 1: + handler = appimage_type_1_create_handler(); + break; + case 2: + handler = appimage_type_2_create_handler(); + break; + default: +#ifdef STANDALONE + fprintf(stderr,"Appimage type %d not supported yet\n", appimage_type); +#endif + handler.traverse = dummy_traverse_func; + break; + } + handler.path = path; + handler.is_open = false; + return handler; +} + +/* + * utility functions + */ +bool is_handler_valid(const appimage_handler *handler) { + if (handler == NULL) { +#ifdef STANDALONE + fprintf(stderr, "WARNING: Invalid handler found, you should take a look at this now!"); +#endif + return false; + } + + return true; +} + +void mk_base_dir(const char *target) +{ + gchar *dirname = g_path_get_dirname(target); + if(g_mkdir_with_parents(dirname, 0755)) { +#ifdef STANDALONE + fprintf(stderr, "Could not create directory: %s\n", dirname); +#endif + } + + g_free(dirname); +} + +/* + * Dummy fallback functions + */ +void dummy_traverse_func(appimage_handler *handler, traverse_cb command, void *data) { + (void) handler; + (void) command; + (void) data; + + fprintf(stderr, "Called %s\n", __FUNCTION__); +} + +char* dummy_get_file_name (appimage_handler *handler, void *data) { + fprintf(stderr, "Called %s\n", __FUNCTION__); +} + +void dummy_extract_file(struct appimage_handler *handler, void *data, char *target) { + fprintf(stderr, "Called %s\n", __FUNCTION__); +} \ No newline at end of file diff --git a/src/libappimage/appimage_handler.h b/src/libappimage/appimage_handler.h new file mode 100644 index 0000000..ceb3f0d --- /dev/null +++ b/src/libappimage/appimage_handler.h @@ -0,0 +1,53 @@ +#pragma once + +// system includes +#include +#include + +/* AppImage generic handler calback to be used in algorithms */ +typedef void (*traverse_cb)(void* handler, void* entry_data, void* user_data); + +/* AppImage generic handler to be used in algorithms */ +struct appimage_handler { + const gchar* path; + + char* (*get_file_name)(struct appimage_handler* handler, void* entry); + + void (*extract_file)(struct appimage_handler* handler, void* entry, const char* target); + + bool (*read_file_into_new_buffer)(struct appimage_handler* handler, void* entry, char** buffer, unsigned long* buffer_size); + + char* (*get_file_link)(struct appimage_handler* handler, void* entry); + + void (*traverse)(struct appimage_handler* handler, traverse_cb command, void* user_data); + + void* cache; + bool is_open; + + // for debugging purposes + int type; +} typedef appimage_handler; + +/* + * appimage_handler functions + */ + +// constructor +appimage_handler create_appimage_handler(const char* const path); + +/* + * utility functions + */ +bool is_handler_valid(const appimage_handler* handler); + +void mk_base_dir(const char *target); + +/* + * dummy fallback callbacks + */ +void dummy_traverse_func(appimage_handler* handler, traverse_cb command, void* data); + +char* dummy_get_file_name(appimage_handler* handler, void* data); + +void dummy_extract_file(struct appimage_handler* handler, void* data, char* target); + diff --git a/src/libappimage/desktop_file_integration_private.h b/src/libappimage/desktop_file_integration_private.h new file mode 100644 index 0000000..91f0d57 --- /dev/null +++ b/src/libappimage/desktop_file_integration_private.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** +* Finds a file with '.Desktop' or '.desktop' suffix. +* @param path +* @return newly allocated const char* with the full path of the file or NULL. Result must be freed. +*/ +char* find_desktop_file(const char* appdir_path); + +GKeyFile* load_desktop_file(const char* file_path); + +#ifdef __cplusplus +} +#endif diff --git a/src/libappimage/desktop_integration.c b/src/libappimage/desktop_integration.c new file mode 100644 index 0000000..2dbd6c4 --- /dev/null +++ b/src/libappimage/desktop_integration.c @@ -0,0 +1,721 @@ +#include +#include + +#include "desktop_integration.h" +#include "appimage_handler.h" +#include "appimage/appimage.h" +#include "libappimage_private.h" + +extern const char* vendorprefix; + +char* desktop_integration_create_tempdir() { + GError* error = NULL; + char* path = g_dir_make_tmp("libappimage-XXXXXX", &error); + if (error) { + g_warning("%s", error->message); + g_error_free(error); + } + + return path; +} + +struct { + const char* tempdir; +} typedef traverse_handler_extract_relevant_desktop_integration_files_data; + +void create_parent_dir(const char* file_name) { + char* parent_dir = g_path_get_dirname(file_name); + if (g_mkdir_with_parents(parent_dir, S_IRWXU) == -1) + g_warning("Unable to create temporary parent dir: %s", parent_dir); + + g_free(parent_dir); +} + +void traverse_handler_extract_relevant_desktop_integration_files(void* raw_handler, void* entry_data, + void* raw_user_data) { + appimage_handler* handler = raw_handler; + traverse_handler_extract_relevant_desktop_integration_files_data* data = raw_user_data; + + char* file_name = handler->get_file_name(handler, entry_data); + if (g_str_has_suffix(file_name, ".Desktop") || + g_str_has_suffix(file_name, ".desktop") || + g_str_has_prefix(file_name, "usr/share/icons") || + g_str_equal(file_name, ".DirIcon")) { + + char* target = g_strjoin("/", data->tempdir, file_name, NULL); + + create_parent_dir(target); + + handler->extract_file(handler, entry_data, target); + + g_free(target); + } + + g_free(file_name); +} + +void desktop_integration_extract_relevant_files(const char* appimage_path, const char* tempdir_path) { + appimage_handler handler = create_appimage_handler(appimage_path); + + traverse_handler_extract_relevant_desktop_integration_files_data data; + data.tempdir = tempdir_path; + handler.traverse(&handler, traverse_handler_extract_relevant_desktop_integration_files, &data); +} + +char* find_desktop_file(const char* path) { + GError* error = NULL; + GDir* temp_dir = g_dir_open(path, 0, &error); + if (error != NULL) { + g_warning("%s\n", error->message); + return NULL; + } + const char* entry = NULL; + while ((entry = g_dir_read_name(temp_dir)) != NULL) { + if (g_str_has_suffix(entry, ".Desktop") || + g_str_has_suffix(entry, ".desktop")) { + break; + } + } + + char* result = NULL; + if (entry != NULL) + result = g_strjoin("/", path, entry, NULL); + + g_dir_close(temp_dir); + return result; +} + +GKeyFile* load_desktop_file(const char* file_path) { + GError* error = NULL; + GKeyFile* desktop_file = g_key_file_new(); + g_key_file_load_from_file(desktop_file, file_path, + G_KEY_FILE_KEEP_COMMENTS | G_KEY_FILE_KEEP_TRANSLATIONS, &error); + if (error) { + g_warning("%s\n", error->message); + g_error_free(error); + } + + return desktop_file; +} + +bool save_desktop_file(GKeyFile* desktop_file, const char* file_path) { + GError* error = NULL; + + g_key_file_save_to_file(desktop_file, file_path, &error); + if (error) { + g_warning("%s\n", error->message); + g_error_free(error); + return false; + } + + return true; +} + +bool move_desktop_file(const char* tempdir_path, const char* user_data_dir, const char* md5sum); + +/** + * Get the path section corresponding to: // + * "/usr/share/icons/hicolor/22x22/apps/appicon.png" + * |_________________| + * this + * */ +char* extract_icon_path_prefix(const char* path) { + char** path_parts = g_strsplit(path, "usr/share/icons/", 2); + char** itr = path_parts; + char* prefix = NULL; + if (*itr != NULL) { + g_free(*itr); + itr++; + } + + if (*itr != NULL) { + prefix = g_path_get_dirname(*itr); + g_free(*itr); + } + + g_free(path_parts); + return prefix; +} + +bool desktop_integration_modify_desktop_file(const char* appimage_path, const char* tempdir_path, const char* md5) { + char* desktop_file_path = find_desktop_file(tempdir_path); + char* desktop_filename = g_path_get_basename(desktop_file_path); + + if (desktop_file_path == NULL) { + g_critical("Failed to find desktop file path\n"); + return false; + } + + if (desktop_filename == NULL) { + g_critical("Failed to query desktop file filename\n"); + return false; + } + + GKeyFile* key_file_structure = load_desktop_file(desktop_file_path); + + if (!g_key_file_has_key(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_EXEC, NULL)) { + g_critical("Desktop file has no Exec key\n"); + return false; + } + + // parse [Try]Exec= value, replace executable by AppImage path, append parameters + // TODO: should respect quoted strings within value + + { + char* field_value = g_key_file_get_value(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, + G_KEY_FILE_DESKTOP_KEY_EXEC, NULL); + + // saving a copy for later free() call + char* original_field_value = field_value; + + char* executable = strsep(&field_value, " "); + + // error handling + if (executable == NULL) { + g_warning("Invalid value for Exec= entry in Desktop file\n"); + return false; + } + + unsigned long new_exec_value_size = strlen(appimage_path) + 1; + + if (field_value != NULL) + new_exec_value_size += strlen(field_value) + 1; + + gchar* new_exec_value = calloc(new_exec_value_size, sizeof(gchar)); + + // build new value + strcpy(new_exec_value, appimage_path); + + if (field_value != NULL && strlen(field_value) > 0) { + strcat(new_exec_value, " "); + strcat(new_exec_value, field_value); + } + + if (original_field_value != NULL) + free(original_field_value); + + g_key_file_set_value(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_EXEC, new_exec_value); + + g_free(new_exec_value); + } + + // force add a TryExec= key + g_key_file_set_value(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_TRY_EXEC, appimage_path); + +#ifdef APPIMAGED + /* If firejail is on the $PATH, then use it to run AppImages */ + if(g_find_program_in_path ("firejail")){ + char *firejail_exec; + firejail_exec = g_strdup_printf("firejail --env=DESKTOPINTEGRATION=appimaged --noprofile --appimage '%s'", appimage_path); + g_key_file_set_value(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_EXEC, firejail_exec); + + gchar *firejail_profile_group = "Desktop Action FirejailProfile"; + gchar *firejail_profile_exec = g_strdup_printf("firejail --env=DESKTOPINTEGRATION=appimaged --private --appimage '%s'", appimage_path); + gchar *firejail_tryexec = "firejail"; + g_key_file_set_value(key_file_structure, firejail_profile_group, G_KEY_FILE_DESKTOP_KEY_NAME, "Run without sandbox profile"); + g_key_file_set_value(key_file_structure, firejail_profile_group, G_KEY_FILE_DESKTOP_KEY_EXEC, firejail_profile_exec); + g_key_file_set_value(key_file_structure, firejail_profile_group, G_KEY_FILE_DESKTOP_KEY_TRY_EXEC, firejail_tryexec); + g_key_file_set_value(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, "Actions", "FirejailProfile;"); + } +#endif + +#ifdef APPIMAGED + /* Add AppImageUpdate desktop action + * https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s10.html + * This will only work if AppImageUpdate is on the user's $PATH. + * TODO: we could have it call this appimaged instance instead of AppImageUpdate and let it + * figure out how to update the AppImage */ + unsigned long upd_offset = 0; + unsigned long upd_length = 0; + if (g_find_program_in_path("AppImageUpdate")) { + if (appimage_type == 2) { + if (!appimage_get_elf_section_offset_and_length(appimage_path, ".upd_info", &upd_offset, &upd_length) || + upd_offset == 0 || upd_length == 0) { + fprintf(stderr, "Could not read .upd_info section in AppImage's header\n"); + } + if (upd_length != 1024) { +#ifdef STANDALONE + fprintf(stderr, + "WARNING: .upd_info length is %lu rather than 1024, this might be a bug in the AppImage\n", + upd_length); +#endif + } + } + if (appimage_type == 1) { + /* If we have a type 1 AppImage, then we hardcode the offset and length */ + upd_offset = 33651; // ISO 9660 Volume Descriptor field + upd_length = 512; // Might be wrong + } +#ifdef STANDALONE + fprintf(stderr, ".upd_info offset: %lu\n", upd_offset); + fprintf(stderr, ".upd_info length: %lu\n", upd_length); +#endif + char buffer[3]; + FILE* binary = fopen(appimage_path, "rt"); + if (binary != NULL) { + /* Check whether the first three bytes at the offset are not NULL */ + fseek(binary, upd_offset, SEEK_SET); + fread(buffer, 1, 3, binary); + fclose(binary); + if ((buffer[0] != 0x00) && (buffer[1] != 0x00) && (buffer[2] != 0x00)) { + gchar* appimageupdate_group = "Desktop Action AppImageUpdate"; + gchar* appimageupdate_exec = g_strdup_printf("%s %s", "AppImageUpdate", appimage_path); + g_key_file_set_value(key_file_structure, appimageupdate_group, G_KEY_FILE_DESKTOP_KEY_NAME, "Update"); + g_key_file_set_value(key_file_structure, appimageupdate_group, G_KEY_FILE_DESKTOP_KEY_EXEC, + appimageupdate_exec); + g_key_file_set_value(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, "Actions", "AppImageUpdate;"); + } + } + } +#endif + + { + // parse desktop files and generate a list of locales representing all localized Name/Icon entries + // NULL refers to the key without the locale tag + // the locales for both entry types need to be tracked separately due to a limitation in the GLib API, see + // the comment below for more information + gchar* nameLocales[256] = {NULL}; + gchar* iconLocales[256] = {NULL}; + gint nameLocalesCount = 1; + gint iconLocalesCount = 1; + + { + // create temporary in-memory copy of the keyfile + gsize bufsize; + char* orig_buffer = g_key_file_to_data(key_file_structure, &bufsize, NULL); + + if (orig_buffer == NULL) { + fprintf(stderr, "Failed to create in-memory copy of desktop file\n"); + return false; + } + + // need to keep original reference to buffer to be able to free() it later + char* buffer = orig_buffer; + + // parse line by line + char* line = NULL; + while ((line = strsep(&buffer, "\n")) != NULL) { + const bool is_name_entry = strncmp(line, "Name[", 5) == 0; + const bool is_icon_entry = strncmp(line, "Icon[", 5) == 0; + + // the only keys for which we're interested in localizations is Name and Icon + if (!(is_name_entry || is_icon_entry)) + continue; + + // python: s = split(line, "[")[1] + char* s = strsep(&line, "["); + s = strsep(&line, "["); + + // python: locale = split(s, "]")[0] + char* locale = strsep(&s, "]"); + + // create references to the right variables + gchar** locales = NULL; + gint* localesCount = NULL; + + if (is_name_entry) { + locales = nameLocales; + localesCount = &nameLocalesCount; + } else if (is_icon_entry) { + locales = iconLocales; + localesCount = &iconLocalesCount; + } + + // avoid duplicates in list of locales + bool is_duplicate = false; + + // unfortunately, the list of locales is not sorted, therefore a linear search is required + // however, this won't have a significant impact on the performance + // start at index 1, first entry is NULL + for (int i = 1; i < *localesCount; i++) { + if (strcmp(locale, locales[i]) == 0) { + is_duplicate = true; + break; + } + } + + if (is_duplicate) + continue; + + locales[(*localesCount)++] = strdup(locale); + } + + free(orig_buffer); + } + + // iterate over locales, check whether name or icon entries exist, and edit accordingly + for (int i = 0; i < iconLocalesCount; i++) { + const gchar* locale = iconLocales[i]; + + // check whether the key is set at all + gchar* old_contents = NULL; + + // it's a little annoying that the GLib functions don't simply work with NULL as the locale, that'd + // make the following if-else construct unnecessary + if (locale == NULL) { + old_contents = g_key_file_get_string( + key_file_structure, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_ICON, NULL + ); + } else { + // please note that the following call will return a value even if there is no localized version + // probably to save one call when you're just interested in getting _some_ value while reading + // a desktop file + old_contents = g_key_file_get_locale_string( + key_file_structure, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_ICON, locale, NULL + ); + } + + // continue to next key if not set + if (old_contents == NULL) { + g_free(old_contents); + continue; + } + + // copy key's original contents + static const gchar old_key[] = "X-AppImage-Old-Icon"; + + // append AppImage version + gchar* basename = g_path_get_basename(old_contents); + gchar* new_contents = g_strdup_printf("%s_%s_%s", vendorprefix, md5, basename); + g_free(basename); + + // see comment for above if-else construct + if (locale == NULL) { + g_key_file_set_string(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, old_key, old_contents); + g_key_file_set_string(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_ICON, + new_contents); + } else { + g_key_file_set_locale_string(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, old_key, locale, + old_contents); + g_key_file_set_locale_string(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_ICON, + locale, new_contents); + } + + // cleanup + g_free(old_contents); + g_free(new_contents); + } + + char* appimage_version = g_key_file_get_string(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, + "X-AppImage-Version", NULL); + // check for name entries and append version suffix + if (appimage_version != NULL) { + for (int i = 0; i < nameLocalesCount; i++) { + const gchar* locale = nameLocales[i]; + + // check whether the key is set at all + gchar* old_contents; + + // it's a little annoying that the GLib functions don't simply work with NULL as the locale, that'd + // make the following if-else construct unnecessary + if (locale == NULL) { + old_contents = g_key_file_get_string( + key_file_structure, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_NAME, NULL + ); + } else { + // please note that the following call will return a value even if there is no localized version + // probably to save one call when you're just interested in getting _some_ value while reading + // a desktop file + old_contents = g_key_file_get_locale_string( + key_file_structure, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_NAME, locale, NULL + ); + } + + // continue to next key if not set + if (old_contents == NULL) { + g_free(old_contents); + continue; + } + + gchar* version_suffix = g_strdup_printf("(%s)", appimage_version); + + // check if version suffix has been appended already + // this makes sure that the version suffix isn't added more than once + if (strlen(version_suffix) > strlen(old_contents) && + strcmp(old_contents + (strlen(old_contents) - strlen(version_suffix)), version_suffix) != 0) { + // copy key's original contents + static const gchar old_key[] = "X-AppImage-Old-Name"; + + // append AppImage version + gchar* new_contents = g_strdup_printf("%s %s", old_contents, version_suffix); + + // see comment for above if-else construct + if (locale == NULL) { + g_key_file_set_string(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, old_key, old_contents); + g_key_file_set_string(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_NAME, + new_contents); + } else { + g_key_file_set_locale_string(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, old_key, locale, + old_contents); + g_key_file_set_locale_string(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, + G_KEY_FILE_DESKTOP_KEY_NAME, locale, new_contents); + } + + // cleanup + g_free(new_contents); + } + + // cleanup + g_free(old_contents); + g_free(version_suffix); + } + } + + for (int i = 1; i < iconLocalesCount; i++) + free(iconLocales[i]); + + for (int i = 1; i < nameLocalesCount; i++) + free(nameLocales[i]); + + // cleanup + g_free(appimage_version); + } + +#ifdef APPIMAGED + { + gchar *generated_by = g_strdup_printf("Generated by appimaged %s", GIT_COMMIT); + g_key_file_set_value(key_file_structure, "Desktop Entry", "X-AppImage-Comment", generated_by); + g_free(generated_by); + } +#endif + g_key_file_set_value(key_file_structure, "Desktop Entry", "X-AppImage-Identifier", md5); + + bool save_result = save_desktop_file(key_file_structure, desktop_file_path); + + // Clean Up + g_key_file_free(key_file_structure); + free(desktop_filename); + free(desktop_file_path); + return save_result; +} + + +char* read_icon_name_from_desktop_file(const char* tempdir_path, const char* appimage_path_md5); + +bool move_app_icons(GSList* path, const char* dir, const char* sum); + +GSList* find_app_icons(const char* tempdir_path, char* icon_name); + +bool move_diricon_as_app_icon(const char* tempdir_path, const char* user_data_dir, const char* appimage_path_md5, + const char* icon_name); + +bool desktop_integration_move_files_to_user_data_dir(const char* tempdir_path, const char* user_data_dir, + const char* appimage_path_md5) { + // Find application icons (in all sizes) + char* icon_name = read_icon_name_from_desktop_file(tempdir_path, appimage_path_md5); + GSList* list = find_app_icons(tempdir_path, icon_name); + + bool res = move_desktop_file(tempdir_path, user_data_dir, appimage_path_md5); + + if (res) { + if (list != NULL) + move_app_icons(list, user_data_dir, appimage_path_md5); + else { + g_warning("No icons found in AppDir/usr/share/icons. Using .DirIcon as fallback"); + move_diricon_as_app_icon(tempdir_path, user_data_dir, appimage_path_md5, icon_name); + } + } + + // TODO: Move MIME-TYPES + + free(icon_name); + return res; +} + +/** + * Move /.DirIcon into /icons/hicolor/32x32/apps/__" + * This function provides a fallback workflow for AppImage that don't properly include their icons. + * + * @param tempdir_path + * @param user_data_dir + * @param appimage_path_md5 + * @param icon_name + */ +bool move_diricon_as_app_icon(const char* tempdir_path, const char* user_data_dir, const char* appimage_path_md5, + const char* icon_name) { + bool success = false; + char* target_dir_path = g_build_path("/", user_data_dir, "icons/hicolor/32x32/apps/", NULL); + g_mkdir_with_parents(target_dir_path, S_IRWXU); + + char* icon_name_with_extension = g_strjoin("", icon_name, ".png", NULL); + char* new_icon_name = g_strjoin("_", vendorprefix, appimage_path_md5, icon_name_with_extension, NULL); + char* target_path = g_build_path("/", target_dir_path, new_icon_name, NULL); + + char* source_path = g_build_path("/", tempdir_path, ".DirIcon", NULL); + + success = move_file(source_path, target_path); + if (!success) + g_warning("Unable to move icon file from %s to %s", source_path, target_path); + + g_free(source_path); + g_free(target_path); + g_free(new_icon_name); + g_free(icon_name_with_extension); + g_free(target_dir_path); + + return success; +} + +char* read_icon_name_from_desktop_file(const char* tempdir_path, const char* appimage_path_md5) { + char* icon_name = NULL; + char* desktop_file_path = find_desktop_file(tempdir_path); + GKeyFile* desktop_file = load_desktop_file(desktop_file_path); + if (desktop_file) { + char* icon_field_value = g_key_file_get_string(desktop_file, + G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_ICON, NULL); + + char* expected_icon_prefix = g_strjoin("_", vendorprefix, appimage_path_md5, "", NULL); + if (g_str_has_prefix(icon_field_value, expected_icon_prefix)) + icon_name = strdup(icon_field_value + strlen(expected_icon_prefix)); + else + icon_name = strdup(icon_field_value); + + g_free(expected_icon_prefix); + g_free(icon_field_value); + } + + g_key_file_free(desktop_file); + g_free(desktop_file_path); + return icon_name; +} + +bool move_icon_file(const char* user_data_dir, const char* appimage_path_md5, const char* path) { + bool succeed = true; + char* prefix = extract_icon_path_prefix(path); + char* file_name = g_path_get_basename(path); + char* new_file_name = g_strjoin("_", vendorprefix, appimage_path_md5, file_name, NULL); + + char* target_dir_path = g_build_path("/", user_data_dir, "icons", prefix, NULL); + char* target_path = g_build_path("/", target_dir_path, new_file_name, NULL); + + if (g_mkdir_with_parents(target_dir_path, S_IRWXU) == -1) { + g_warning("Unable to create dir: %s\n", target_dir_path); + succeed = false; + } + + if (!move_file(path, target_path)) { + g_warning("Unable to move icon to: %s\n", target_path); + succeed = false; + } + + g_free(target_path); + g_free(target_dir_path); + g_free(new_file_name); + g_free(file_name); + g_free(prefix); + return succeed; +} + +/** + * Look for file icons named in the /usr/share/icons. + * + * @param tempdir_path + * @param icon_name + * @return GSList of char*. Use `g_slist_free_full(list, &g_free);` to free it. + */ +GSList* find_app_icons(const char* tempdir_path, char* icon_name) { + char* icons_dir_path = g_build_path("/", tempdir_path, "usr/share/icons", NULL); + + GSList* list = NULL; + GQueue* dirs_queue = g_queue_new(); + g_queue_push_head(dirs_queue, icons_dir_path); + + while (!g_queue_is_empty(dirs_queue)) { + char* current_dir_path = g_queue_pop_head(dirs_queue); + GDir* current_dir = g_dir_open(current_dir_path, 0, NULL); + + const char* entry = NULL; + // Iterate directory entries + while ((entry = g_dir_read_name(current_dir)) != NULL) { + char* path = g_build_path("/", current_dir_path, entry, NULL); + + if (g_file_test(path, G_FILE_TEST_IS_DIR)) + g_queue_push_head(dirs_queue, path); + else { + if (g_str_has_prefix(entry, icon_name)) + list = g_slist_append(list, path); + } + + } + + g_dir_close(current_dir); + g_free(current_dir_path); + } + + g_queue_free(dirs_queue); + return list; +} + +/** + * Move icons files listed in the to the keeping the // + * file structure and appends the prefix "__" to the file name. + * + * @param icon_files list of icon files to be moved + * @param user_data_dir directory where the desktop integration files will be deployed usually "$HOME/.local/shared" + * @param appimage_path_md5 + * @param icon_name application icon name, extracted from the .desktop file + * @return true on success otherwise false + * */ +bool move_app_icons(GSList* icon_files, const char* user_data_dir, const char* appimage_path_md5) { + bool succeed = true; + for (GSList* itr = icon_files; itr != NULL & succeed; itr = itr->next) { + const char* path = itr->data; + succeed = move_icon_file(user_data_dir, appimage_path_md5, path); + } + + return succeed; +} + +/** + * Move a the application .desktop file to /applications/__ + * @param tempdir_path + * @param user_data_dir + * @param md5sum + */ +bool move_desktop_file(const char* tempdir_path, const char* user_data_dir, const char* md5sum) { + bool succeed = false; + char* target_dir_path = g_build_path("/", user_data_dir, "applications", NULL); + succeed = g_mkdir_with_parents(target_dir_path, S_IRWXU) == 0; + + char* desktop_file_path = find_desktop_file(tempdir_path); + char* desktop_filename = g_path_get_basename(desktop_file_path); + + char* target_desktop_filename = g_strdup_printf("%s_%s-%s", vendorprefix, md5sum, desktop_filename); + free(desktop_filename); + + char* target_desktop_file_path = g_build_path("/", target_dir_path, target_desktop_filename, NULL); + free(target_desktop_filename); + free(target_dir_path); + + succeed = move_file(desktop_file_path, target_desktop_file_path); + + free(desktop_file_path); + free(target_desktop_file_path); + + return succeed; +} + +void desktop_integration_remove_tempdir(const char* dir_path) { + GDir* tempdir = NULL; + GError* error; + tempdir = g_dir_open(dir_path, 0, &error); + if (!tempdir) { + g_warning("%s\n", error->message); + g_error_free(error); + return; + } + + const char* entry; + while ((entry = g_dir_read_name(tempdir)) != NULL) { + char* full_path = g_strjoin("/", dir_path, entry, NULL); + if (g_file_test(full_path, G_FILE_TEST_IS_DIR)) { + desktop_integration_remove_tempdir(full_path); + } else + g_remove(full_path); + + free(full_path); + } + + g_rmdir(dir_path); + g_dir_close(tempdir); +} diff --git a/src/libappimage/desktop_integration.h b/src/libappimage/desktop_integration.h new file mode 100644 index 0000000..866b9b9 --- /dev/null +++ b/src/libappimage/desktop_integration.h @@ -0,0 +1,66 @@ +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * Functions related to the desktop integration of AppImages. + */ + +/** + * Create a temporary dir to extract the AppImage Desktop Integration relate files such as: + * - .Desktop file + * - .AppIcon + * - usr/share/icons/... + * + * @param appimage_path + * @return path to the temporary dir + */ +char* desktop_integration_create_tempdir(); + +/** + * Extract the AppImage Desktop Integration relate files such as: + * - .Desktop file + * - .AppIcon + * - usr/share/icons/... + * + * @param appimage_path + * @param tempdir_path + */ +void desktop_integration_extract_relevant_files(const char* appimage_path, const char* tempdir_path); + + +/** + * Modifies the fields on the .Desktop file to make them point to the expected locations of the desktop integration + * files. + * Fields modified: + * - Exec: will point to the AppImage + * - TryExcec: will point to the AppImage + * - Icon: Will point to the expected path of the application icon in $XDG_DATA_HOME/icons/.../apps/.. + * @param appimage_path + * @param tempdir_path + */ +bool desktop_integration_modify_desktop_file(const char* appimage_path, const char* tempdir_path, const char* md5); + + +/** + * Move .desktop file and application icons from /share to the $XDG_DATA_HOME + * @param tempdir_path + * @param user_data_dir + * @param md5sum + */ +bool desktop_integration_move_files_to_user_data_dir(const char* tempdir_path, const char* user_data_dir, + const char* md5sum); + +/** + * Remove recusively remaining files at the temporary dir and the dir itself + * @param dir_path + */ +void desktop_integration_remove_tempdir(const char* dir_path); + +#ifdef __cplusplus +} +#endif diff --git a/src/libappimage/libappimage.c b/src/libappimage/libappimage.c new file mode 100644 index 0000000..a289a02 --- /dev/null +++ b/src/libappimage/libappimage.c @@ -0,0 +1,2069 @@ +/************************************************************************** + * + * Copyright (c) 2004-18 Simon Peter + * + * All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + **************************************************************************/ + +#ident "AppImage by Simon Peter, http://appimage.org/" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "squashfuse.h" +#include +#include "elf.h" + +#include "xdg-basedir.h" +#include "appimage_handler.h" + +// own header +#include "appimage/appimage.h" +#include "desktop_integration.h" +#include "type1.h" + +#if HAVE_LIBARCHIVE3 == 1 // CentOS +# include +# include +#else // other systems +# include +# include +#endif + +#include +#include + +#include // To get the size of icons, move_icon_to_destination() + +#define FNM_FILE_NAME 2 + +#define URI_MAX (FILE_MAX * 3 + 8) + +char *vendorprefix = "appimagekit"; + +/* Search and replace on a string, this really should be in Glib */ +gchar* replace_str(const gchar const *src, const gchar const *find, const gchar const *replace){ + gchar **split = g_strsplit(src, find, -1); + gchar *retval = g_strjoinv(replace, split); + + g_strfreev(split); + return retval; +} + +/* Return the md5 hash constructed according to + * https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html#THUMBSAVE + * This can be used to identify files that are related to a given AppImage at a given location */ +char *appimage_get_md5(const char* path) +{ + char* absolute_path; + + if ((absolute_path = realpath(path, NULL)) == NULL) + absolute_path = strdup(path); + + gchar *uri = g_filename_to_uri(absolute_path, NULL, NULL); + + free(absolute_path); + + if (uri != NULL) + { + GChecksum *checksum; + checksum = g_checksum_new(G_CHECKSUM_MD5); + guint8 digest[16]; + gsize digest_len = sizeof (digest); + g_checksum_update(checksum, (const guchar *) uri, strlen (uri)); + g_checksum_get_digest(checksum, digest, &digest_len); + g_assert(digest_len == 16); + gchar *result = g_strdup(g_checksum_get_string(checksum)); + g_checksum_free(checksum); + g_free(uri); + return result; + } else { + return NULL; + } +} + +/* Return the path of the thumbnail regardless whether it already exists; may be useful because + * G*_FILE_ATTRIBUTE_THUMBNAIL_PATH only exists if the thumbnail is already there. + * Check libgnomeui/gnome-thumbnail.h for actually generating thumbnails in the correct + * sizes at the correct locations automatically; which would draw in a dependency on gdk-pixbuf. + */ +char *get_thumbnail_path(const char *path, char *thumbnail_size, gboolean verbose) +{ + char *md5 = appimage_get_md5(path); + char *file = g_strconcat(md5, ".png", NULL); + char* cache_home = xdg_cache_home(); + gchar *thumbnail_path = g_build_filename(cache_home, "thumbnails", thumbnail_size, file, NULL); + g_free(cache_home); + g_free(file); + g_free(md5); + return thumbnail_path; +} + +/* Move an icon file to the path where a given icon can be installed in $HOME. + * This is needed because png and xpm icons cannot be installed in a generic + * location but are only picked up in directories that have the size of + * the icon as part of their directory name, as specified in the theme.index + * See https://github.com/AppImage/AppImageKit/issues/258 + */ + +void move_icon_to_destination(gchar *icon_path, gboolean verbose) +{ + // FIXME: This default location is most likely wrong, but at least the icons with unknown size can go somewhere + char* data_home = xdg_data_home(); + gchar *dest_dir = g_build_path("/", data_home, "/icons/hicolor/48x48/apps/", NULL); + + if((g_str_has_suffix (icon_path, ".svg")) || (g_str_has_suffix (icon_path, ".svgz"))) { + g_free(dest_dir); + dest_dir = g_build_path("/", data_home, "/icons/hicolor/scalable/apps/", NULL); + } + g_free(data_home); + + if((g_str_has_suffix (icon_path, ".png")) || (g_str_has_suffix (icon_path, ".xpm"))) { + + cairo_surface_t *image; + + int w = 0; + int h = 0; + + if(g_str_has_suffix (icon_path, ".xpm")) { + // TODO: GdkPixbuf has a convenient way to load XPM data. Then you can call + // gdk_cairo_set_source_pixbuf() to transfer the data to a Cairo surface. +#ifdef STANDALONE + fprintf(stderr, "XPM size parsing not yet implemented\n"); +#endif + } + + if(g_str_has_suffix (icon_path, ".png")) { + image = cairo_image_surface_create_from_png(icon_path); + w = cairo_image_surface_get_width (image); + h = cairo_image_surface_get_height (image); + cairo_surface_destroy (image); + } + + // FIXME: The following sizes are taken from the hicolor icon theme. + // Probably the right thing to do would be to figure out at runtime which icon sizes are allowable. + // Or could we put our own index.theme into .local/share/icons/ and have it observed? + if((w != h) || ((w != 16) && (w != 24) && (w != 32) && (w != 36) && (w != 48) && (w != 64) && (w != 72) && (w != 96) && (w != 128) && (w != 192) && (w != 256) && (w != 512))){ +#ifdef STANDALONE + fprintf(stderr, "%s has nonstandard size w = %i, h = %i; please fix it\n", icon_path, w, h); +#endif + } else { + g_free(dest_dir); + char* data_home = xdg_data_home(); + dest_dir = g_build_path("/", data_home, "/icons/hicolor/", g_strdup_printf("%ix%i", w, h), "/apps/", NULL); + free(data_home); + } + } + if(verbose) + fprintf(stderr, "dest_dir %s\n", dest_dir); + + gchar *basename = g_path_get_basename(icon_path); + + gchar* icon_dest_path = g_build_path("/", dest_dir, basename, NULL); + + g_free(basename); + if(verbose) + fprintf(stderr, "Move from %s to %s\n", icon_path, icon_dest_path); + gchar *dirname = g_path_get_dirname(dest_dir); + if(g_mkdir_with_parents(dirname, 0755)) { +#ifdef STANDALONE + fprintf(stderr, "Could not create directory: %s\n", dest_dir); +#endif + } + + g_free(dirname); + g_free(dest_dir); + + // This is required only for old versions of glib2 and is harmless for newer + g_type_init(); + + GError *error = NULL; + GFile *icon_file = g_file_new_for_path(icon_path); + GFile *target_file = g_file_new_for_path(icon_dest_path); + if(!g_file_move(icon_file, target_file, G_FILE_COPY_OVERWRITE, NULL, NULL, NULL, &error)){ +#ifdef STANDALONE + fprintf(stderr, "Error moving file: %s\n", error->message); +#endif + g_error_free(error); + } + g_object_unref(icon_file); + g_object_unref(target_file); + g_free(icon_dest_path); + +} + +/* Get filename extension */ +static gchar *get_file_extension(const gchar *filename) +{ + gchar **tokens; + gchar *extension; + tokens = g_strsplit(filename, ".", 2); + if (tokens[0] == NULL) + extension = NULL; + else + extension = g_strdup(tokens[1]); + g_strfreev(tokens); + return extension; +} + +// Read the file in chunks +void squash_extract_inode_to_file(sqfs *fs, sqfs_inode *inode, const gchar *dest) { + off_t bytes_already_read = 0; + sqfs_off_t bytes_at_a_time = 64*1024; + FILE * f; + f = fopen (dest, "w+"); + if (f == NULL){ +#ifdef STANDALONE + fprintf(stderr, "fopen error\n"); +#endif + return; + } + while (bytes_already_read < (*inode).xtra.reg.file_size) + { + char buf[bytes_at_a_time]; + if (sqfs_read_range(fs, inode, (sqfs_off_t) bytes_already_read, &bytes_at_a_time, buf) != SQFS_OK) { +#ifdef STANDALONE + fprintf(stderr, "sqfs_read_range error\n"); +#endif + } + fwrite(buf, 1, (size_t) bytes_at_a_time, f); + bytes_already_read = bytes_already_read + bytes_at_a_time; + } + fclose(f); +} + +/* Find files in the squashfs matching to the regex pattern. + * Returns a newly-allocated NULL-terminated array of strings. + * Use g_strfreev() to free it. + * + * The following is done within the sqfs_traverse run for performance reaons: + * 1.) For found files that are in usr/share/icons, install those icons into the system + * with a custom name that involves the md5 identifier to tie them to a particular + * AppImage. + * 2.) For found files that are in usr/share/mime/packages, install those icons into the system + * with a custom name that involves the md5 identifier to tie them to a particular + * AppImage. + */ +gchar **squash_get_matching_files_install_icons_and_mime_data(sqfs* fs, char* pattern, + gchar* desktop_icon_value_original, char* md5, + gboolean verbose) { + GPtrArray *array = g_ptr_array_new(); + sqfs_traverse trv; + sqfs_err err = sqfs_traverse_open(&trv, fs, sqfs_inode_root(fs)); + if (err!= SQFS_OK) { +#ifdef STANDALONE + fprintf(stderr, "sqfs_traverse_open error\n"); +#endif + } + while (sqfs_traverse_next(&trv, &err)) { + if (!trv.dir_end) { + int r; + regex_t regex; + regmatch_t match[2]; + regcomp(®ex, pattern, REG_ICASE | REG_EXTENDED); + r = regexec(®ex, trv.path, 2, match, 0); + regfree(®ex); + sqfs_inode inode; + if(sqfs_inode_get(fs, &inode, trv.entry.inode)) { +#ifdef STANDALONE + fprintf(stderr, "sqfs_inode_get error\n"); +#endif + } + if(r == 0){ + // We have a match + if(verbose) + fprintf(stderr, "squash_get_matching_files found: %s\n", trv.path); + g_ptr_array_add(array, g_strdup(trv.path)); + gchar *dest = NULL; + if(inode.base.inode_type == SQUASHFS_REG_TYPE || inode.base.inode_type == SQUASHFS_LREG_TYPE) { + if(g_str_has_prefix(trv.path, "usr/share/icons/") || g_str_has_prefix(trv.path, "usr/share/pixmaps/") || (g_str_has_prefix(trv.path, "usr/share/mime/") && g_str_has_suffix(trv.path, ".xml"))){ + char* data_home = xdg_data_home(); + gchar *path = replace_str(trv.path, "usr/share", data_home); + free(data_home); + char *dest_dirname = g_path_get_dirname(path); + g_free(path); + gchar *base_name = g_path_get_basename(trv.path); + gchar *dest_basename = g_strdup_printf("%s_%s_%s", vendorprefix, md5, base_name); + + dest = g_build_path("/", dest_dirname, dest_basename, NULL); + + g_free(base_name); + g_free(dest_basename); + } + /* According to https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html#install_icons + * share/pixmaps is ONLY searched in /usr but not in $XDG_DATA_DIRS and hence $HOME and this seems to be true at least in XFCE */ + if(g_str_has_prefix (trv.path, "usr/share/pixmaps/")){ + gchar *dest_basename = g_strdup_printf("%s_%s_%s", vendorprefix, md5, g_path_get_basename(trv.path)); + + dest = g_build_path("/", "/tmp", dest_basename, NULL); + + g_free(dest_basename); + } + /* Some AppImages only have the icon in their root directory, so we have to get it from there */ + if((g_str_has_prefix(trv.path, desktop_icon_value_original)) && (! strstr(trv.path, "/")) && ( (g_str_has_suffix(trv.path, ".png")) || (g_str_has_suffix(trv.path, ".xpm")) || (g_str_has_suffix(trv.path, ".svg")) || (g_str_has_suffix(trv.path, ".svgz")))){ + gchar *ext = get_file_extension(trv.path); + gchar *dest_basename = g_strdup_printf("%s_%s_%s.%s", vendorprefix, md5, desktop_icon_value_original, ext); + + dest = g_build_path("/", "/tmp", dest_basename, NULL); + + g_free(dest_basename); + g_free(ext); + } + + if(dest){ + if(verbose) + fprintf(stderr, "install: %s\n", dest); + + gchar *dirname = g_path_get_dirname(dest); + if(g_mkdir_with_parents(dirname, 0755)) { +#ifdef STANDALONE + fprintf(stderr, "Could not create directory: %s\n", dirname); +#endif + } + + g_free(dirname); + + squash_extract_inode_to_file(fs, &inode, dest); + + chmod (dest, 0644); + + if(verbose) + fprintf(stderr, "Installed: %s\n", dest); + + // If we were unsure about the size of an icon, we temporarily installed + // it to /tmp and now move it into the proper place + if(g_str_has_prefix (dest, "/tmp/")) { + move_icon_to_destination(dest, verbose); + } + + g_free(dest); + } + } + } + } + } + g_ptr_array_add(array, NULL); + if (err) { +#ifdef STANDALONE + fprintf(stderr, "sqfs_traverse_next error\n"); +#endif + } + sqfs_traverse_close(&trv); + return (gchar **) g_ptr_array_free(array, FALSE); +} + + + +/** + * Lookup a given in . If the path points to a symlink it is followed until a regular file is found. + * This method is aware of symlink loops and will fail properly in such case. + * @param fs + * @param path + * @param inode [RETURN PARAMETER] will be filled with a regular file inode. It cannot be NULL + * @return succeed true if the file is found, otherwise false + */ +bool sqfs_lookup_path_resolving_symlinks(sqfs* fs, char* path, sqfs_inode* inode) { + g_assert(fs != NULL); + g_assert(inode != NULL); + + bool found = false; + sqfs_inode root_inode; + sqfs_err err = sqfs_inode_get(fs, &root_inode, fs->sb.root_inode); + if (err != SQFS_OK) { +#ifdef STANDALONE + g_warning("sqfs_inode_get root inode error\n"); +#endif + return false; + } + + *inode = root_inode; + err = sqfs_lookup_path(fs, inode, path, &found); + + if (!found) { +#ifdef STANDALONE + g_warning("sqfs_lookup_path path not found: %s\n", path); +#endif + return false; + } + + if (err != SQFS_OK) { +#ifdef STANDALONE + g_warning("sqfs_lookup_path error: %s\n", path); +#endif + + return false; + } + + // Save visited inode numbers to prevent loops + GSList* inodes_visited = g_slist_append(NULL, (gpointer) inode->base.inode_number); + + while (inode->base.inode_type == SQUASHFS_SYMLINK_TYPE || inode->base.inode_type == SQUASHFS_LSYMLINK_TYPE) { + // Read symlink + size_t size; + // read twice, once to find out right amount of memory to allocate + err = sqfs_readlink(fs, inode, NULL, &size); + if (err != SQFS_OK) { +#ifdef STANDALONE + fprintf(stderr, "sqfs_readlink error: %s\n", path); +#endif + g_slist_free(inodes_visited); + return false; + } + + char symlink_target_path[size]; + // then to populate the buffer + err = sqfs_readlink(fs, inode, symlink_target_path, &size); + if (err != SQFS_OK) { +#ifdef STANDALONE + g_warning("sqfs_readlink error: %s\n", path); +#endif + g_slist_free(inodes_visited); + return false; + } + + // lookup symlink target path + *inode = root_inode; + err = sqfs_lookup_path(fs, inode, symlink_target_path, &found); + + if (!found) { +#ifdef STANDALONE + g_warning("sqfs_lookup_path path not found: %s\n", symlink_target_path); +#endif + g_slist_free(inodes_visited); + return false; + } + + if (err != SQFS_OK) { +#ifdef STANDALONE + g_warning("sqfs_lookup_path error: %s\n", symlink_target_path); +#endif + g_slist_free(inodes_visited); + return false; + } + + // check if we felt into a loop + if (g_slist_find(inodes_visited, (gconstpointer) inode->base.inode_number)) { +#ifdef STANDALONE + g_warning("Symlinks loop found while trying to resolve: %s", path); +#endif + g_slist_free(inodes_visited); + return false; + } else + inodes_visited = g_slist_append(inodes_visited, (gpointer) inode->base.inode_number); + } + + g_slist_free(inodes_visited); + return true; +} + +/** + * Read a regular from in chunks of and merge them into one. + * + * @param fs + * @param inode + * @param buffer [RETURN PARAMETER] + * @param buffer_size [RETURN PARAMETER] + * @return succeed true, buffer pointing to the memory, buffer_size holding the actual size in memory + * if all was ok. Otherwise succeed false, buffer pointing NULL and buffer_size = 0. + * The buffer MUST BE FREED using g_free(). + */ +bool sqfs_read_regular_inode(sqfs* fs, sqfs_inode *inode, char **buffer, off_t *buffer_size) { + GSList *blocks = NULL; + + off_t bytes_already_read = 0; + unsigned long read_buf_size = 256*1024; + + // This has a double role in sqfs_read_range it's used to set the max_bytes_to_be_read and + // after complete it's set to the number ob bytes actually read. + sqfs_off_t size = 0; + sqfs_err err; + + // Read chunks until the end of the file. + do { + size = read_buf_size; + char* buf_read = malloc(sizeof(char) * size); + if (buf_read != NULL) { + err = sqfs_read_range(fs, inode, (sqfs_off_t) bytes_already_read, &size, buf_read); + if (err != SQFS_OK) { +#ifdef STANDALONE + g_warning("sqfs_read_range failed\n"); +#endif + } + else + blocks = g_slist_append(blocks, buf_read); + bytes_already_read += size; + } else { // handle not enough memory properly +#ifdef STANDALONE + g_warning("sqfs_read_regular_inode: Unable to allocate enough memory.\n"); +#endif + err = SQFS_ERR; + } + } while ( (err == SQFS_OK) && (size == read_buf_size) ); + + + bool succeed = false; + *buffer_size = 0; + *buffer = NULL; + + if (err == SQFS_OK) { + // Put all the memory blocks together + guint length = g_slist_length(blocks); + + *buffer = malloc(sizeof(char) * bytes_already_read); + if (*buffer != NULL) { // Prevent crash if the + GSList *ptr = blocks; + for (int i = 0; i < (length-1); i ++) { + memcpy(*buffer + (i*read_buf_size), ptr->data, read_buf_size); + ptr = ptr->next; + } + + memcpy(*buffer + ((length-1)*read_buf_size), ptr->data, (size_t) size); + + succeed = true; + *buffer_size = bytes_already_read; + } else { // handle not enough memory properly +#ifdef STANDALONE + g_warning("sqfs_read_regular_inode: Unable to allocate enough memory.\n"); +#endif + succeed = false; + } + } + + g_slist_free_full(blocks, &g_free); + return succeed; +} + +/** + * Loads a desktop file from into an empty GKeyFile structure. In case of symlinks they are followed. + * This function is capable of detecting loops and will return false in such cases. + * + * @param fs + * @param path + * @param key_file_structure [OUTPUT PARAMETER] + * @param verbose + * @return true if all when ok, otherwise false. + */ +gboolean g_key_file_load_from_squash(sqfs* fs, char* path, GKeyFile* key_file_structure, gboolean verbose) { + sqfs_inode inode; + if (!sqfs_lookup_path_resolving_symlinks(fs, path, &inode)) + return false; + + gboolean success = false; + if (inode.base.inode_type == SQUASHFS_REG_TYPE || inode.base.inode_type == SQUASHFS_LREG_TYPE ) { + char* buf = NULL; + off_t buf_size; + sqfs_read_regular_inode(fs, &inode, &buf, &buf_size); + if (buf != NULL) { + success = g_key_file_load_from_data(key_file_structure, buf, (gsize) buf_size, + G_KEY_FILE_KEEP_COMMENTS | G_KEY_FILE_KEEP_TRANSLATIONS, NULL); + + g_free(buf); + } else + success = false; + } + + return success; +} + +gchar *build_installed_desktop_file_path(gchar* md5, gchar* desktop_filename) { + gchar *partial_path; + partial_path = g_strdup_printf("applications/appimagekit_%s-%s", md5, desktop_filename); + + char *data_home = xdg_data_home(); + gchar *destination = g_build_filename(data_home, partial_path, NULL); + g_free(data_home); + + g_free(partial_path); + + return destination; +} + +/* Write a modified desktop file to disk that points to the AppImage */ +bool write_edited_desktop_file(GKeyFile *key_file_structure, const char* appimage_path, gchar* desktop_filename, int appimage_type, char *md5, gboolean verbose) { + if(!g_key_file_has_key(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_EXEC, NULL)){ +#ifdef STANDALONE + fprintf(stderr, "Desktop file has no Exec key\n"); +#endif + return false; + } + + // parse [Try]Exec= value, replace executable by AppImage path, append parameters + // TODO: should respect quoted strings within value + + { + char* field_value = g_key_file_get_value(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_EXEC, NULL); + + // saving a copy for later free() call + char* original_field_value = field_value; + + char* executable = strsep(&field_value, " "); + + // error handling + if (executable == NULL) { +#ifdef STANDALONE + fprintf(stderr, "Invalid value for Exec= entry in Desktop file\n"); +#endif + return false; + } + + unsigned long new_exec_value_size = strlen(appimage_path) + 1; + + if (field_value != NULL) + new_exec_value_size += strlen(field_value) + 1; + + gchar* new_exec_value = calloc(new_exec_value_size, sizeof(gchar)); + + // build new value + strcpy(new_exec_value, appimage_path); + + if (field_value != NULL && strlen(field_value) > 0) { + strcat(new_exec_value, " "); + strcat(new_exec_value, field_value); + } + + if (original_field_value != NULL) + free(original_field_value); + + g_key_file_set_value(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_EXEC, new_exec_value); + + g_free(new_exec_value); + } + + // force add a TryExec= key + g_key_file_set_value(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_TRY_EXEC, appimage_path); + +#ifdef APPIMAGED + /* If firejail is on the $PATH, then use it to run AppImages */ + if(g_find_program_in_path ("firejail")){ + char *firejail_exec; + firejail_exec = g_strdup_printf("firejail --env=DESKTOPINTEGRATION=appimaged --noprofile --appimage '%s'", appimage_path); + g_key_file_set_value(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_EXEC, firejail_exec); + + gchar *firejail_profile_group = "Desktop Action FirejailProfile"; + gchar *firejail_profile_exec = g_strdup_printf("firejail --env=DESKTOPINTEGRATION=appimaged --private --appimage '%s'", appimage_path); + gchar *firejail_tryexec = "firejail"; + g_key_file_set_value(key_file_structure, firejail_profile_group, G_KEY_FILE_DESKTOP_KEY_NAME, "Run without sandbox profile"); + g_key_file_set_value(key_file_structure, firejail_profile_group, G_KEY_FILE_DESKTOP_KEY_EXEC, firejail_profile_exec); + g_key_file_set_value(key_file_structure, firejail_profile_group, G_KEY_FILE_DESKTOP_KEY_TRY_EXEC, firejail_tryexec); + g_key_file_set_value(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, "Actions", "FirejailProfile;"); + } +#endif + +#ifdef APPIMAGED + /* Add AppImageUpdate desktop action + * https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s10.html + * This will only work if AppImageUpdate is on the user's $PATH. + * TODO: we could have it call this appimaged instance instead of AppImageUpdate and let it + * figure out how to update the AppImage */ + unsigned long upd_offset = 0; + unsigned long upd_length = 0; + if(g_find_program_in_path ("AppImageUpdate")){ + if(appimage_type == 2){ + if (!appimage_get_elf_section_offset_and_length(appimage_path, ".upd_info", &upd_offset, &upd_length) || upd_offset == 0 || upd_length == 0) { + fprintf(stderr, "Could not read .upd_info section in AppImage's header\n"); + } + if(upd_length != 1024) { +#ifdef STANDALONE + fprintf(stderr, + "WARNING: .upd_info length is %lu rather than 1024, this might be a bug in the AppImage\n", + upd_length); +#endif + } + } + if(appimage_type == 1){ + /* If we have a type 1 AppImage, then we hardcode the offset and length */ + upd_offset = 33651; // ISO 9660 Volume Descriptor field + upd_length = 512; // Might be wrong + } +#ifdef STANDALONE + fprintf(stderr, ".upd_info offset: %lu\n", upd_offset); + fprintf(stderr, ".upd_info length: %lu\n", upd_length); +#endif + char buffer[3]; + FILE *binary = fopen(appimage_path, "rt"); + if (binary != NULL) + { + /* Check whether the first three bytes at the offset are not NULL */ + fseek(binary, upd_offset, SEEK_SET); + fread(buffer, 1, 3, binary); + fclose(binary); + if((buffer[0] != 0x00) && (buffer[1] != 0x00) && (buffer[2] != 0x00)){ + gchar *appimageupdate_group = "Desktop Action AppImageUpdate"; + gchar *appimageupdate_exec = g_strdup_printf("%s %s", "AppImageUpdate", appimage_path); + g_key_file_set_value(key_file_structure, appimageupdate_group, G_KEY_FILE_DESKTOP_KEY_NAME, "Update"); + g_key_file_set_value(key_file_structure, appimageupdate_group, G_KEY_FILE_DESKTOP_KEY_EXEC, appimageupdate_exec); + g_key_file_set_value(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, "Actions", "AppImageUpdate;"); + } + } + } +#endif + + { + // parse desktop files and generate a list of locales representing all localized Name/Icon entries + // NULL refers to the key without the locale tag + // the locales for both entry types need to be tracked separately due to a limitation in the GLib API, see + // the comment below for more information + gchar* nameLocales[256] = {NULL}; + gchar* iconLocales[256] = {NULL}; + gint nameLocalesCount = 1; + gint iconLocalesCount = 1; + + { + // create temporary in-memory copy of the keyfile + gsize bufsize; + char* orig_buffer = g_key_file_to_data(key_file_structure, &bufsize, NULL); + + if (orig_buffer == NULL) { + fprintf(stderr, "Failed to create in-memory copy of desktop file\n"); + return false; + } + + // need to keep original reference to buffer to be able to free() it later + char* buffer = orig_buffer; + + // parse line by line + char* line = NULL; + while ((line = strsep(&buffer, "\n")) != NULL) { + const bool is_name_entry = strncmp(line, "Name[", 5) == 0; + const bool is_icon_entry = strncmp(line, "Icon[", 5) == 0; + + // the only keys for which we're interested in localizations is Name and Icon + if (!(is_name_entry || is_icon_entry)) + continue; + + // python: s = split(line, "[")[1] + char* s = strsep(&line, "["); + s = strsep(&line, "["); + + // python: locale = split(s, "]")[0] + char* locale = strsep(&s, "]"); + + // create references to the right variables + gchar** locales = NULL; + gint* localesCount = NULL; + + if (is_name_entry) { + locales = nameLocales; + localesCount = &nameLocalesCount; + } else if (is_icon_entry) { + locales = iconLocales; + localesCount = &iconLocalesCount; + } + + // avoid duplicates in list of locales + bool is_duplicate = false; + + // unfortunately, the list of locales is not sorted, therefore a linear search is required + // however, this won't have a significant impact on the performance + // start at index 1, first entry is NULL + for (int i = 1; i < *localesCount; i++) { + if (strcmp(locale, locales[i]) == 0) { + is_duplicate = true; + break; + } + } + + if (is_duplicate) + continue; + + locales[(*localesCount)++] = strdup(locale); + } + + free(orig_buffer); + } + + // iterate over locales, check whether name or icon entries exist, and edit accordingly + for (int i = 0; i < iconLocalesCount; i++) { + const gchar* locale = iconLocales[i]; + + // check whether the key is set at all + gchar* old_contents = NULL; + + // it's a little annoying that the GLib functions don't simply work with NULL as the locale, that'd + // make the following if-else construct unnecessary + if (locale == NULL) { + old_contents = g_key_file_get_string( + key_file_structure, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_ICON, NULL + ); + } else { + // please note that the following call will return a value even if there is no localized version + // probably to save one call when you're just interested in getting _some_ value while reading + // a desktop file + old_contents = g_key_file_get_locale_string( + key_file_structure, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_ICON, locale, NULL + ); + } + + // continue to next key if not set + if (old_contents == NULL) { + g_free(old_contents); + continue; + } + + // copy key's original contents + static const gchar old_key[] = "X-AppImage-Old-Icon"; + + // append AppImage version + gchar* basename = g_path_get_basename(old_contents); + gchar* new_contents = g_strdup_printf("%s_%s_%s", vendorprefix, md5, basename); + g_free(basename); + + // see comment for above if-else construct + if (locale == NULL) { + g_key_file_set_string(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, old_key, old_contents); + g_key_file_set_string(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_ICON, + new_contents); + } else { + g_key_file_set_locale_string(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, old_key, locale, old_contents); + g_key_file_set_locale_string(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_ICON, locale, new_contents); + } + + // cleanup + g_free(old_contents); + g_free(new_contents); + } + + char* appimage_version = g_key_file_get_string(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, "X-AppImage-Version", NULL); + // check for name entries and append version suffix + if (appimage_version != NULL) { + for (int i = 0; i < nameLocalesCount; i++) { + const gchar* locale = nameLocales[i]; + + // check whether the key is set at all + gchar* old_contents; + + // it's a little annoying that the GLib functions don't simply work with NULL as the locale, that'd + // make the following if-else construct unnecessary + if (locale == NULL) { + old_contents = g_key_file_get_string( + key_file_structure, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_NAME, NULL + ); + } else { + // please note that the following call will return a value even if there is no localized version + // probably to save one call when you're just interested in getting _some_ value while reading + // a desktop file + old_contents = g_key_file_get_locale_string( + key_file_structure, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_NAME, locale, NULL + ); + } + + // continue to next key if not set + if (old_contents == NULL) { + g_free(old_contents); + continue; + } + + gchar* version_suffix = g_strdup_printf("(%s)", appimage_version); + + // check if version suffix has been appended already + // this makes sure that the version suffix isn't added more than once + if (strlen(version_suffix) > strlen(old_contents) && strcmp(old_contents + (strlen(old_contents) - strlen(version_suffix)), version_suffix) != 0) { + // copy key's original contents + static const gchar old_key[] = "X-AppImage-Old-Name"; + + // append AppImage version + gchar* new_contents = g_strdup_printf("%s %s", old_contents, version_suffix); + + // see comment for above if-else construct + if (locale == NULL) { + g_key_file_set_string(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, old_key, old_contents); + g_key_file_set_string(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_NAME, new_contents); + } else { + g_key_file_set_locale_string(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, old_key, locale, old_contents); + g_key_file_set_locale_string(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_NAME, locale, new_contents); + } + + // cleanup + g_free(new_contents); + } + + // cleanup + g_free(old_contents); + g_free(version_suffix); + } + } + + for (int i = 1; i < iconLocalesCount; i++) + free(iconLocales[i]); + + for (int i = 1; i < nameLocalesCount; i++) + free(nameLocales[i]); + + // cleanup + g_free(appimage_version); + } + +#ifdef APPIMAGED + { + gchar *generated_by = g_strdup_printf("Generated by appimaged %s", GIT_COMMIT); + g_key_file_set_value(key_file_structure, "Desktop Entry", "X-AppImage-Comment", generated_by); + g_free(generated_by); + } +#endif + g_key_file_set_value(key_file_structure, "Desktop Entry", "X-AppImage-Identifier", md5); +#ifdef STANDALONE + fprintf(stderr, "Installing desktop file\n"); +#endif + if(verbose) { + gchar *buf = g_key_file_to_data(key_file_structure, NULL, NULL); + fprintf(stderr, "%s", buf); + g_free(buf); + } + + /* https://specifications.freedesktop.org/menu-spec/menu-spec-latest.html#paths says: + * + * $XDG_DATA_DIRS/applications/ + * When two desktop entries have the same name, the one appearing earlier in the path is used. + * + * -- + * + * https://developer.gnome.org/integration-guide/stable/desktop-files.html.en says: + * + * Place this file in the /usr/share/applications directory so that it is accessible + * by everyone, or in ~/.local/share/applications if you only wish to make it accessible + * to a single user. Which is used should depend on whether your application is + * installed systemwide or into a user's home directory. GNOME monitors these directories + * for changes, so simply copying the file to the right location is enough to register it + * with the desktop. + * + * Note that the ~/.local/share/applications location is not monitored by versions of GNOME + * prior to version 2.10 or on Fedora Core Linux, prior to version 2.8. + * These versions of GNOME follow the now-deprecated vfolder standard, + * and so desktop files must be installed to ~/.gnome2/vfolders/applications. + * This location is not supported by GNOME 2.8 on Fedora Core nor on upstream GNOME 2.10 + * so for maximum compatibility with deployed desktops, put the file in both locations. + * + * Note that the KDE Desktop requires one to run kbuildsycoca to force a refresh of the menus. + * + * -- + * + * https://specifications.freedesktop.org/menu-spec/menu-spec-latest.html says: + * + * To prevent that a desktop entry from one party inadvertently cancels out + * the desktop entry from another party because both happen to get the same + * desktop-file id it is recommended that providers of desktop-files ensure + * that all desktop-file ids start with a vendor prefix. + * A vendor prefix consists of [a-zA-Z] and is terminated with a dash ("-"). + * For example, to ensure that GNOME applications start with a vendor prefix of "gnome-", + * it could either add "gnome-" to all the desktop files it installs + * in datadir/applications/ or it could install desktop files in a + * datadir/applications/gnome subdirectory. + * + * -- + * + * https://specifications.freedesktop.org/desktop-entry-spec/latest/ape.html says: + * The desktop file ID is the identifier of an installed desktop entry file. + * + * To determine the ID of a desktop file, make its full path relative + * to the $XDG_DATA_DIRS component in which the desktop file is installed, + * remove the "applications/" prefix, and turn '/' into '-'. + * For example /usr/share/applications/foo/bar.desktop has the desktop file ID + * foo-bar.desktop. + * If multiple files have the same desktop file ID, the first one in the + * $XDG_DATA_DIRS precedence order is used. + * For example, if $XDG_DATA_DIRS contains the default paths + * /usr/local/share:/usr/share, then /usr/local/share/applications/org.foo.bar.desktop + * and /usr/share/applications/org.foo.bar.desktop both have the same desktop file ID + * org.foo.bar.desktop, but only the first one will be used. + * + * -- + * + * https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s07.html says: + * + * The application must name its desktop file in accordance with the naming + * recommendations in the introduction section (e.g. the filename must be like + * org.example.FooViewer.desktop). The application must have a D-Bus service + * activatable at the well-known name that is equal to the desktop file name + * with the .desktop portion removed (for our example, org.example.FooViewer). + * + * -- + * + * Can it really be that no one thought about having multiple versions of the same + * application installed? What are we supposed to do if we want + * a) have desktop files installed by appimaged not interfere with desktop files + * provided by the system, i.e., if an application is installed in the system + * and the user also installs the AppImage, then both should be available to the user + * b) both should be D-Bus activatable + * c) the one installed by appimaged should have an AppImage vendor prefix to make + * it easy to distinguish it from system- or upstream-provided ones + */ + + /* FIXME: The following is most likely not correct; see the comments above. + * Open a GitHub issue or send a pull request if you would like to propose asolution. */ + /* TODO: Check for consistency of the id with the AppStream file, if it exists in the AppImage */ + gchar *destination = build_installed_desktop_file_path(md5, desktop_filename); + + /* When appimaged sees itself, then do nothing here */ + if(strcmp ("appimaged.desktop", desktop_filename) == 0) { + g_free(destination); +#ifdef STANDALONE + fprintf(stderr, "appimaged's desktop file found -- not installing desktop file for myself\n"); +#endif + return true; + } + + if(verbose) + fprintf(stderr, "install: %s\n", destination); + + gchar *dirname = g_path_get_dirname(destination); + if(g_mkdir_with_parents(dirname, 0755)) { +#ifdef STANDALONE + fprintf(stderr, "Could not create directory: %s\n", dirname); +#endif + } + g_free(dirname); + + // g_key_file_save_to_file(key_file_structure, destination, NULL); + // g_key_file_save_to_file is too new, only since 2.40 + /* Write config file on disk */ + gsize length; + gchar *buf; + GIOChannel *file; + buf = g_key_file_to_data(key_file_structure, &length, NULL); + file = g_io_channel_new_file(destination, "w", NULL); + g_io_channel_write_chars(file, buf, length, NULL, NULL); + g_io_channel_shutdown(file, TRUE, NULL); + g_io_channel_unref(file); + + g_free(buf); + + /* GNOME shows the icon and name on the desktop file only if it is executable */ + chmod(destination, 0755); + + g_free(destination); + + return true; +} + +bool appimage_type1_get_desktop_filename_and_key_file(struct archive** a, gchar** desktop_filename, GKeyFile** key_file) { + // iterate over all files ("entries") in the archive + // looking for a file with .desktop extension in the root directory + + // must not be freed + struct archive_entry* entry; + + gchar* filename; + + for (;;) { + int r = archive_read_next_header(*a, &entry); + + if (r == ARCHIVE_EOF) { + return false; + } + + if (r != ARCHIVE_OK) { + fprintf(stderr, "%s\n", archive_error_string(*a)); + return false; + } + + /* Skip all but regular files; FIXME: Also handle symlinks correctly */ + if (archive_entry_filetype(entry) != AE_IFREG) + continue; + + filename = replace_str(archive_entry_pathname(entry), "./", ""); + + /* Get desktop file(s) in the root directory of the AppImage and act on it in one go */ + if ((g_str_has_suffix(filename, ".desktop") && (NULL == strstr(filename, "/")))) { +#ifdef STANDALONE + fprintf(stderr, "Got root desktop: %s\n", filename); +#endif + + const void* buff; + + size_t size = 1024 * 1024; + int64_t offset = 0; + + r = archive_read_data_block(*a, &buff, &size, &offset); + + if (r == ARCHIVE_EOF) { + // cleanup + g_free(filename); + + return true; + } + + if (r != ARCHIVE_OK) { + fprintf(stderr, "%s", archive_error_string(*a)); + break; + } + + *desktop_filename = g_path_get_basename(filename); + + // a structure that will hold the information from the desktop file + *key_file = g_key_file_new(); + + gboolean success = g_key_file_load_from_data(*key_file, buff, size, + G_KEY_FILE_KEEP_COMMENTS | G_KEY_FILE_KEEP_TRANSLATIONS, NULL); + + if (!success) { + // cleanup + g_free(key_file); + key_file = NULL; + + break; + } + + // cleanup + g_free(filename); + + return true; + } + } + + g_free(filename); + + return false; +} + +/* Register a type 1 AppImage in the system + * DEPRECATED, it should be removed ASAP + * */ +bool appimage_type1_register_in_system(const char *path, bool verbose) +{ + return appimage_register_in_system(path, verbose) == 0; +} + +bool appimage_type2_get_desktop_filename_and_key_file(sqfs* fs, gchar** desktop_filename, gchar* md5, GKeyFile** key_file, gboolean verbose) { + /* TOOO: Change so that only one run of squash_get_matching_files is needed in total, + * this should hopefully improve performance */ + + /* Get desktop file(s) in the root directory of the AppImage */ + // Only in root dir + gchar** str_array = squash_get_matching_files_install_icons_and_mime_data(fs, "(^[^/]*?.desktop$)", "", md5, verbose); + + bool errored = false; + + // gchar **str_array = squash_get_matching_files(&fs, "(^.*?.desktop$)", md5, verbose); // Not only there + /* Work trough the NULL-terminated array of strings */ + for (int i = 0; str_array[i]; ++i) { +#ifdef STANDALONE + fprintf(stderr, "Got root desktop: %s\n", str_array[i]); +#endif + + if (!g_key_file_load_from_squash(fs, str_array[i], *key_file, verbose)) + errored = true; + else + *desktop_filename = g_path_get_basename(str_array[i]); + } + + /* Free the NULL-terminated array of strings and its contents */ + g_strfreev(str_array); + + return !errored; +} + +/* Register a type 2 AppImage in the system + * DEPRECATED it should be removed ASAP + * */ +bool appimage_type2_register_in_system(const char *path, bool verbose) { + return appimage_register_in_system(path, verbose) == 0; +} + +int appimage_type1_is_terminal_app(const char* path) { + // check if file exists + if (!g_file_test(path, G_FILE_TEST_IS_REGULAR)) + return -1; + + // check if file is of correct type + if (appimage_get_type(path, false) != 1) + return -1; + + char* md5 = appimage_get_md5(path); + + if (md5 == NULL) + return -1; + + // open ISO9660 image using libarchive + struct archive *a = archive_read_new(); + archive_read_support_format_iso9660(a); + + // libarchive status int -- passed to called functions + int r; + + if ((r = archive_read_open_filename(a, path, 10240)) != ARCHIVE_OK) { + // cleanup + free(md5); + archive_read_free(a); + + return -1; + } + // search image for root desktop file, and read it into key file structure so it can be edited eventually + gchar *desktop_filename = NULL; + GKeyFile *key_file = NULL; + + if (!appimage_type1_get_desktop_filename_and_key_file(&a, &desktop_filename, &key_file)) { + // cleanup + free(md5); + archive_read_free(a); + g_free(desktop_filename); + g_key_file_free(key_file); + + return -1; + } + + // validate that both have been set to a non-NULL value + if (desktop_filename == NULL || key_file == NULL) { + // cleanup + free(md5); + archive_read_free(a); + g_free(desktop_filename); + g_key_file_free(key_file); + + return -1; + } + + GError *error = NULL; + gboolean rv = g_key_file_get_boolean(key_file, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_TERMINAL, &error); + + // cleanup + free(md5); + archive_read_free(a); + g_free(desktop_filename); + g_key_file_free(key_file); + + int result; + + if (!rv) { + // if the key file hasn't been found and the error is not set to NOT_FOUND, return an error + if (error != NULL && error->code != G_KEY_FILE_ERROR_KEY_NOT_FOUND) + result = -1; + else + result = 0; + } else { + result = 1; + } + + if (error != NULL) + g_error_free(error); + + return result; +}; + +int appimage_type2_is_terminal_app(const char* path) { + // check if file exists + if (!g_file_test(path, G_FILE_TEST_IS_REGULAR)) + return -1; + + // check if file is of correct type + if (appimage_get_type(path, false) != 2) + return -1; + + char* md5 = appimage_get_md5(path); + + if (md5 == NULL) + return -1; + + ssize_t fs_offset = appimage_get_elf_size(path); + + // error check + if (fs_offset < 0) + return -1; + + sqfs fs; + + sqfs_err err = sqfs_open_image(&fs, path, (size_t) fs_offset); + + if (err != SQFS_OK) { + free(md5); + sqfs_destroy(&fs); + return -1; + } + + gchar* desktop_filename = NULL; + + // a structure that will hold the information from the desktop file + GKeyFile* key_file = g_key_file_new(); + + if (!appimage_type2_get_desktop_filename_and_key_file(&fs, &desktop_filename, md5, &key_file, false)) { + // cleanup + free(md5); + free(desktop_filename); + sqfs_destroy(&fs); + g_key_file_free(key_file); + + return -1; + } + + // validate that both have been set to a non-NULL value + if (desktop_filename == NULL || key_file == NULL) { + // cleanup + free(md5); + sqfs_destroy(&fs); + g_free(desktop_filename); + g_key_file_free(key_file); + + return -1; + } + + // no longer used + free(md5); + + GError *error = NULL; + gboolean rv = g_key_file_get_boolean(key_file, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_TERMINAL, &error); + + // cleanup + free(desktop_filename); + sqfs_destroy(&fs); + g_key_file_free(key_file); + + int result; + + if (!rv) { + // if the key file hasn't been found and the error is not set to NOT_FOUND, return an error + if (error != NULL && error->code != G_KEY_FILE_ERROR_KEY_NOT_FOUND) + result = -1; + else + result = 0; + } else { + result = 1; + } + + if (error != NULL) + g_error_free(error); + + return result; +}; + +/* + * Checks whether an AppImage's desktop file has set Terminal=true. + * Useful to check whether the author of an AppImage doesn't want it to be integrated. + * + * Returns >0 if set, 0 if not set, <0 on errors. + */ +int appimage_is_terminal_app(const char *path) { + // check if file exists + if (!g_file_test(path, G_FILE_TEST_IS_REGULAR)) + return -1; + + int type = appimage_get_type(path, false); + + switch (type) { + case 1: + return appimage_type1_is_terminal_app(path); + case 2: + return appimage_type2_is_terminal_app(path); + default: + return -1; + } +} + +int appimage_type1_shall_not_be_integrated(const char* path) { + // check if file exists + if (!g_file_test(path, G_FILE_TEST_IS_REGULAR)) + return -1; + + // check if file is of correct type + if (appimage_get_type(path, false) != 1) + return -1; + + char* md5 = appimage_get_md5(path); + + if (md5 == NULL) + return -1; + + // open ISO9660 image using libarchive + struct archive *a = archive_read_new(); + archive_read_support_format_iso9660(a); + + // libarchive status int -- passed to called functions + int r; + + if ((r = archive_read_open_filename(a, path, 10240)) != ARCHIVE_OK) { + // cleanup + free(md5); + archive_read_free(a); + + return -1; + } + // search image for root desktop file, and read it into key file structure so it can be edited eventually + gchar *desktop_filename = NULL; + GKeyFile *key_file = NULL; + + if (!appimage_type1_get_desktop_filename_and_key_file(&a, &desktop_filename, &key_file)) { + // cleanup + free(md5); + archive_read_free(a); + g_free(desktop_filename); + g_key_file_free(key_file); + + return -1; + } + + // validate that both have been set to a non-NULL value + if (desktop_filename == NULL || key_file == NULL) { + // cleanup + free(md5); + archive_read_free(a); + g_free(desktop_filename); + g_key_file_free(key_file); + + return -1; + } + + GError *error = NULL; + gboolean rv = g_key_file_get_boolean(key_file, G_KEY_FILE_DESKTOP_GROUP, "X-AppImage-Integrate", &error); + + // cleanup + free(md5); + archive_read_free(a); + g_free(desktop_filename); + g_key_file_free(key_file); + + int result; + + if (!rv) { + // if the key file hasn't been found and the error is not set to NOT_FOUND, return an error + if (error != NULL) { + if (error->code == G_KEY_FILE_ERROR_KEY_NOT_FOUND) + result = 0; + else + result = -1; + } + else { + result = 1; + } + } else { + result = 0; + } + + if (error != NULL) + g_error_free(error); + + return result; +}; + +int appimage_type2_shall_not_be_integrated(const char* path) { + // check if file exists + if (!g_file_test(path, G_FILE_TEST_IS_REGULAR)) + return -1; + + // check if file is of correct type + if (appimage_get_type(path, false) != 2) + return -1; + + char* md5 = appimage_get_md5(path); + + if (md5 == NULL) + return -1; + + ssize_t fs_offset = appimage_get_elf_size(path); + + if (fs_offset < 0) + return -1; + + sqfs fs; + + sqfs_err err = sqfs_open_image(&fs, path, (size_t) fs_offset); + + if (err != SQFS_OK) { + free(md5); + sqfs_destroy(&fs); + return -1; + } + + gchar* desktop_filename = NULL; + + // a structure that will hold the information from the desktop file + GKeyFile* key_file = g_key_file_new(); + + if (!appimage_type2_get_desktop_filename_and_key_file(&fs, &desktop_filename, md5, &key_file, false)) { + // cleanup + free(md5); + free(desktop_filename); + sqfs_destroy(&fs); + g_key_file_free(key_file); + + return -1; + } + + // validate that both have been set to a non-NULL value + if (desktop_filename == NULL || key_file == NULL) { + // cleanup + free(md5); + sqfs_destroy(&fs); + g_free(desktop_filename); + g_key_file_free(key_file); + + return -1; + } + + // no longer used + free(md5); + + GError *error = NULL; + gboolean rv = g_key_file_get_boolean(key_file, G_KEY_FILE_DESKTOP_GROUP, "X-AppImage-Integrate", &error); + + // cleanup + free(desktop_filename); + sqfs_destroy(&fs); + g_key_file_free(key_file); + + int result; + + if (!rv) { + // if the key file hasn't been found and the error is not set to NOT_FOUND, return an error + if (error != NULL) { + if (error->code == G_KEY_FILE_ERROR_KEY_NOT_FOUND) + result = 0; + else + result = -1; + } else { + result = 1; + } + } else { + result = 0; + } + + if (error != NULL) + g_error_free(error); + + return result; +}; + +/* + * Checks whether an AppImage's desktop file has set X-AppImage-Integrate=false. + * Useful to check whether the author of an AppImage doesn't want it to be integrated. + * + * Returns >0 if set, 0 if not set, <0 on errors. + */ +int appimage_shall_not_be_integrated(const char *path) { + // check if file exists + if (!g_file_test(path, G_FILE_TEST_IS_REGULAR)) + return -1; + + int type = appimage_get_type(path, false); + + switch (type) { + case 1: + return appimage_type1_shall_not_be_integrated(path); + case 2: + return appimage_type2_shall_not_be_integrated(path); + default: + return -1; + } +} + +char* appimage_registered_desktop_file_path(const char *path, char *md5, bool verbose) { + glob_t pglob = {}; + + // if md5 has been calculated before, we can just use it to save these extra calculations + // if not, we need to calculate it here + if (md5 == NULL) + md5 = appimage_get_md5(path); + + // sanity check + if (md5 == NULL) { + if (verbose) + fprintf(stderr, "appimage_get_md5() failed\n"); + return NULL; + } + + char *data_home = xdg_data_home(); + + // TODO: calculate this value exactly + char *glob_pattern = malloc(PATH_MAX); + sprintf(glob_pattern, "%s/applications/appimagekit_%s-*.desktop", data_home, md5); + + glob(glob_pattern, 0, NULL, &pglob); + + char* rv = NULL; + + if (pglob.gl_pathc <= 0) { + if (verbose) { + fprintf(stderr, "No results found by glob()"); + } + } else if (pglob.gl_pathc >= 1) { + if (pglob.gl_pathc > 1 && verbose) { + fprintf(stderr, "Too many results returned by glob(), returning first result found"); + } + + // need to copy value to be able to globfree() later on + rv = strdup(pglob.gl_pathv[0]); + } + + globfree(&pglob); + + return rv; +}; + +/* Check whether AppImage is registered in the system already */ +bool appimage_is_registered_in_system(const char* path) { + // To check whether an AppImage has been integrated, we just have to check whether the desktop file is in place + + if (!g_file_test(path, G_FILE_TEST_IS_REGULAR)) + return false; + + gchar* md5 = appimage_get_md5(path); + + GKeyFile* key_file = g_key_file_new(); + gchar* desktop_file_path = appimage_registered_desktop_file_path(path, md5, false); + + bool rv = true; + + if (!g_file_test(desktop_file_path, G_FILE_TEST_IS_REGULAR)) + rv = false; + + g_free(md5); + g_free(desktop_file_path); + g_key_file_free(key_file); + + return rv; +} + +/* + * Register an AppImage in the system + * Returns 0 on success, non-0 otherwise. + */ +int appimage_register_in_system(const char* path, bool verbose) { + if ((g_str_has_suffix(path, ".part")) || + g_str_has_suffix(path, ".tmp") || + g_str_has_suffix(path, ".download") || + g_str_has_suffix(path, ".zs-old") || + g_str_has_suffix(path, ".~") + ) { + return 1; + } + + int type = appimage_get_type(path, verbose); + bool succeed = true; + + if (type != -1) { +#ifdef STANDALONE + fprintf(stderr, "\n-> Registering type %d AppImage: %s\n", type, path); +#endif + appimage_create_thumbnail(path, false); + + char* temp_dir = desktop_integration_create_tempdir(); + char* md5 = appimage_get_md5(path); + char* data_home = xdg_data_home(); + + // Files are extracted to a temporary dir to avoid several traversals on the AppImage file + // Also, they need to be edited by us anyway, and to avoid confusing desktop environments with + // too many different desktop files, we edit them beforehand and move them into their target + // destination afterwards only. + // (Yes, it _could_ probably be done without tempfiles, but given the amount of desktop registrations, + // we consider the file I/O overhead to be acceptable.) + desktop_integration_extract_relevant_files(path, temp_dir); + succeed = succeed && desktop_integration_modify_desktop_file(path, temp_dir, md5); + succeed = succeed && desktop_integration_move_files_to_user_data_dir(temp_dir, data_home, md5); + desktop_integration_remove_tempdir(temp_dir); + + free(data_home); + free(md5); + free(temp_dir); + } else { +#ifdef STANDALONE + fprintf(stderr, "Error: unknown AppImage type %d\n", type); +#endif + if (verbose) + fprintf(stderr, "-> Skipping file %s\n", path); + return 0; + } + + return succeed ? 0 : 1; +} + +/* Delete the thumbnail for a given file and size if it exists */ +void delete_thumbnail(char *path, char *size, gboolean verbose) +{ + gchar *thumbnail_path = get_thumbnail_path(path, size, verbose); + if(verbose) + fprintf(stderr, "get_thumbnail_path: %s\n", thumbnail_path); + if(g_file_test(thumbnail_path, G_FILE_TEST_IS_REGULAR)){ + g_unlink(thumbnail_path); + if(verbose) + fprintf(stderr, "deleted: %s\n", thumbnail_path); + } + g_free(thumbnail_path); +} + +/* Recursively delete files in path and subdirectories that contain the given md5 + */ +void unregister_using_md5_id(const char *name, int level, char* md5, gboolean verbose) +{ + DIR *dir; + struct dirent *entry; + + if (!(dir = opendir(name))) + return; + if (!(entry = readdir(dir))) + return; + + do { + if (entry->d_type == DT_DIR) { + char path[1024]; + int len = snprintf(path, sizeof(path)-1, "%s/%s", name, entry->d_name); + path[len] = 0; + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) + continue; + unregister_using_md5_id(path, level + 1, md5, verbose); + } + + else { + gchar *needle = g_strdup_printf("%s_%s", vendorprefix, md5); + if(strstr(entry->d_name, needle)) { + gchar *path_to_be_deleted = g_strdup_printf("%s/%s", name, entry->d_name); + if(g_file_test(path_to_be_deleted, G_FILE_TEST_IS_REGULAR)){ + g_unlink(path_to_be_deleted); + if(verbose) + fprintf(stderr, "deleted: %s\n", path_to_be_deleted); + } + g_free(path_to_be_deleted); + } + g_free(needle); + } + } while ((entry = readdir(dir)) != NULL); + closedir(dir); +} + + +/* Unregister an AppImage in the system */ +int appimage_unregister_in_system(const char *path, bool verbose) +{ + char *md5 = appimage_get_md5(path); + + /* The file is already gone by now, so we can't determine its type anymore */ +#ifdef STANDALONE + fprintf(stderr, "_________________________\n"); + fprintf(stderr, "\n"); + fprintf(stderr, "-> UNREGISTER %s\n", path); +#endif + /* Could use gnome_desktop_thumbnail_factory_lookup instead of the next line */ + + /* Delete the thumbnails if they exist */ + delete_thumbnail(path, "normal", verbose); // 128x128 + delete_thumbnail(path, "large", verbose); // 256x256 + + char* data_home = xdg_data_home(); + unregister_using_md5_id(data_home, 0, md5, verbose); + g_free(data_home); + + g_free(md5); + + return 0; +} + +bool move_file(const char* source, const char* target) { + g_type_init(); + bool succeed = true; + GError *error = NULL; + GFile *icon_file = g_file_new_for_path(source); + GFile *target_file = g_file_new_for_path(target); + if (!g_file_move (icon_file, target_file, G_FILE_COPY_OVERWRITE, NULL, NULL, NULL, &error)) { +#ifdef STANDALONE + fprintf(stderr, "Error moving file: %s\n", error->message); +#endif + succeed = false; + g_clear_error (&error); + } + + g_object_unref(icon_file); + g_object_unref(target_file); + + return succeed; +} + +struct extract_appimage_file_command_data { + const char *path; + const char *destination; + char *link; +}; +struct read_appimage_file_into_buffer_command_data { + char* file_path; + char* out_buffer; + char* link_path; + unsigned long out_buf_size; + bool success; +}; + +void extract_appimage_file_command(void* handler_data, void* entry_data, void* user_data) { + appimage_handler* h = handler_data; + struct extract_appimage_file_command_data * params = user_data; + + char* filename = h->get_file_name(h, entry_data); + if (strcmp(params->path, filename) == 0) { + params->link = h->get_file_link(handler_data, entry_data); + + h->extract_file(h, entry_data, params->destination); + } + + + free(filename); +} + +void read_appimage_file_into_buffer_command(void* handler_data, void* entry_data, void* user_data) { + appimage_handler* h = handler_data; + struct read_appimage_file_into_buffer_command_data* params = user_data; + + if (h->read_file_into_new_buffer == NULL) { +#ifdef STANDALONE + fprintf(stderr, "read_file_into_new_buffer is NULL, go fix that!\n"); +#endif + return; + } + + char* filename = h->get_file_name(h, entry_data); + if (strcmp(params->file_path, filename) == 0) { + params->link_path = h->get_file_link(h, entry_data); + params->success = h->read_file_into_new_buffer(h, entry_data, ¶ms->out_buffer, &(params->out_buf_size)); + } + + + free(filename); +} + +void extract_appimage_icon_command(void *handler_data, void *entry_data, void *user_data) { + appimage_handler *h = handler_data; + struct archive_entry *entry = entry_data; + gchar *path = user_data; + + char *filename = h->get_file_name(h, entry); + if (strcmp(".DirIcon", filename) == 0) + h->extract_file(h, entry, path); + + free(filename); +} + +void extract_appimage_icon(appimage_handler *h, gchar *target) { + h->traverse(h, extract_appimage_icon_command, target); +} + +/* Create AppImage thumbanil according to + * https://specifications.freedesktop.org/thumbnail-spec/0.8.0/index.html + */ +void appimage_create_thumbnail(const char *appimage_file_path, bool verbose) { + // extract AppImage icon to /tmp + appimage_handler handler = create_appimage_handler(appimage_file_path); + + char *tmp_path = "/tmp/appimage_thumbnail_tmp"; + extract_appimage_icon(&handler, tmp_path); + + if (g_file_test(tmp_path, G_FILE_TEST_EXISTS) ) { + // TODO: transform it to png with sizes 128x128 and 254x254 + gchar *target_path = get_thumbnail_path(appimage_file_path, "normal", verbose); + + mk_base_dir(target_path); + + // deploy icon as thumbnail + move_file (tmp_path, target_path); + + // clean up + g_free(target_path); + } else { +#ifdef STANDALONE + fprintf(stderr, "ERROR: Icon file not extracted: %s", tmp_path); +#endif + } + +} + +void appimage_extract_file_following_symlinks(const gchar* appimage_file_path, const char* file_path, + const char* target_file_path) { + + struct extract_appimage_file_command_data data; + data.link = strdup(file_path); + data.destination = target_file_path; + + bool looping = false; + GSList* visited_entries = NULL; + + do { + visited_entries = g_slist_prepend(visited_entries, data.link); + data.path = data.link; + data.link = NULL; + + appimage_handler handler = create_appimage_handler(appimage_file_path); + handler.traverse(&handler, extract_appimage_file_command, &data); + + if (data.link != NULL) { + if (visited_entries != NULL && g_slist_find_custom(visited_entries, data.link, (GCompareFunc) strcmp)) + looping = true; + + g_remove(target_file_path); + } + + } while (data.link != NULL && !looping); + + if (visited_entries != NULL) + g_slist_free_full(visited_entries, free); +} + +bool appimage_read_file_into_buffer_following_symlinks(const char* appimage_file_path, const char* file_path, + char** buffer, unsigned long* buf_size) { + + struct read_appimage_file_into_buffer_command_data data; + data.link_path = strdup(file_path); + + data.out_buffer = NULL; + GSList *visited_entries = NULL; + + do { + visited_entries = g_slist_prepend(visited_entries, data.link_path); + + // prepare an empty struct + data.file_path = data.link_path; + data.link_path = NULL; + + // release any data that could be allocated in previous iterations + if (data.out_buffer != NULL) { + free(data.out_buffer); + data.out_buffer = NULL; + data.out_buf_size = 0; + } + + data.success = false; + + appimage_handler handler = create_appimage_handler(appimage_file_path); + handler.traverse(&handler, &read_appimage_file_into_buffer_command, &data); + + // Find loops + if (data.link_path != NULL && visited_entries && + g_slist_find_custom(visited_entries, data.link_path, (GCompareFunc) strcmp)) + data.success = false; + } while (data.success && data.link_path != NULL); + + if (visited_entries != NULL) + g_slist_free_full(visited_entries, free); + + if (!data.success) { + free(data.out_buffer); + + *buffer = NULL; + *buf_size = 0; + } else { + *buffer = data.out_buffer; + *buf_size = data.out_buf_size; + } + + return data.success; +} + +void extract_appimage_file_name(void *handler_data, void *entry_data, void *user_data) { + appimage_handler *h = handler_data; + struct archive_entry *entry = entry_data; + GList **list = user_data; + + char *filename = h->get_file_name(h, entry); + + GList* ptr = g_list_find_custom (*list, filename, g_strcmp0); + + if (ptr == NULL) + *list = g_list_append(*list, filename); + else + free(filename); +} + + +char** appimage_list_files(const char *path) { + GList *list = NULL; + appimage_handler handler = create_appimage_handler(path); + + handler.traverse(&handler, extract_appimage_file_name, &list); + + int n = g_list_length(list); + char **result = malloc(sizeof(char*) * (n+1) ); + result[n] = NULL; + + GList *itr = list; + for (int i = 0; i < n; i ++) { + result[i] = (char *) itr->data; + itr = itr->next; + } + + + g_list_free(list); + + return result; +} + +void appimage_string_list_free(char** list) { + for (char **ptr = list; ptr != NULL && *ptr != NULL; ptr ++) + free(*ptr); + + free(list); +} + + +/* Check if a file is an AppImage. Returns the image type if it is, or -1 if it isn't */ +int appimage_get_type(const char* path, bool verbose) +{ + FILE *f = fopen(path, "rt"); + if (f != NULL) + { + char buffer[3] = {0}; + + /* Check magic bytes at offset 8 */ + fseek(f, 8, SEEK_SET); + fread(buffer, 1, 3, f); + fclose(f); + if(match_type_1_magic_bytes(buffer)){ +#ifdef STANDALONE + fprintf(stderr, "_________________________\n"); +#endif + if(verbose){ + fprintf(stderr, "AppImage type 1\n"); + } + return 1; + } else if((buffer[0] == 0x41) && (buffer[1] == 0x49) && (buffer[2] == 0x02)){ +#ifdef STANDALONE + fprintf(stderr, "_________________________\n"); +#endif + if(verbose){ + fprintf(stderr, "AppImage type 2\n"); + } + return 2; + } else { + if (is_iso_9660_file(path) && (appimage_get_elf_size(path) != -1)) { +#ifdef STANDALONE + fprintf(stderr, "_________________________\n"); +#endif + if (verbose) { + fprintf(stderr, "This file seems to be an AppImage type 1 without magic bytes\n"); + fprintf(stderr, "The AppImage author should embed the magic bytes," + " see https://github.com/AppImage/AppImageSpec\n"); + } + return 1; + } else { +#ifdef STANDALONE + fprintf(stderr, "_________________________\n"); +#endif + if(verbose){ + fprintf(stderr, "Unrecognized file '%s'\n", path); + } + return -1; + } + } + } + return -1; +} diff --git a/src/libappimage/libappimage_private.h b/src/libappimage/libappimage_private.h new file mode 100644 index 0000000..0aceb6a --- /dev/null +++ b/src/libappimage/libappimage_private.h @@ -0,0 +1 @@ +bool move_file(const char* source, const char* target); diff --git a/src/libappimage/type1.c b/src/libappimage/type1.c new file mode 100644 index 0000000..1b09f00 --- /dev/null +++ b/src/libappimage/type1.c @@ -0,0 +1,205 @@ +// system includes +#include + +// library includes +#include +#include + +// local includes +#include "type2.h" +#include "type1.h" + + +void appimage_type1_open(appimage_handler* handler) { + if (is_handler_valid(handler) && !handler->is_open) { +#ifdef STANDALONE + fprintf(stderr, "Opening %s as Type 1 AppImage\n", handler->path); +#endif + struct archive* a; + a = archive_read_new(); + archive_read_support_format_iso9660(a); + if (archive_read_open_filename(a, handler->path, 10240) != ARCHIVE_OK) { + fprintf(stderr, "%s", archive_error_string(a)); + handler->cache = NULL; + handler->is_open = false; + } else { + handler->cache = a; + handler->is_open = true; + } + } +} + +void appimage_type1_close(appimage_handler* handler) { + if (is_handler_valid(handler) && handler->is_open) { +#ifdef STANDALONE + fprintf(stderr, "Closing %s\n", handler->path); +#endif + struct archive* a = handler->cache; + archive_read_close(a); + archive_read_free(a); + + handler->cache = NULL; + handler->is_open = false; + } +} + +void type1_traverse(appimage_handler* handler, traverse_cb command, void* command_data) { + appimage_type1_open(handler); + + if (!command) { +#ifdef STANDALONE + fprintf(stderr, "No traverse command set.\n"); +#endif + return; + } + + if (handler->is_open) { + struct archive* a = handler->cache; + struct archive_entry* entry; + int r; + + for (;;) { + r = archive_read_next_header(a, &entry); + if (r == ARCHIVE_EOF) { + break; + } + if (r != ARCHIVE_OK) { + fprintf(stderr, "%s\n", archive_error_string(a)); + break; + } + + /* Skip all but regular files; FIXME: Also handle symlinks correctly */ + if (archive_entry_filetype(entry) != AE_IFREG) { + continue; + } + + command(handler, entry, command_data); + } + } + + appimage_type1_close(handler); +} + +// TODO: remove forward declaration +gchar* replace_str(const gchar* src, const gchar* find, const gchar* replace); + +char* type1_get_file_name(appimage_handler* handler, void* data) { + (void) handler; + + struct archive_entry* entry = (struct archive_entry*) data; + + char* filename = replace_str(archive_entry_pathname(entry), "./", ""); + return filename; +} + +void type1_extract_file(appimage_handler* handler, void* data, const char* target) { + (void) data; + + struct archive* a = handler->cache; + mk_base_dir(target); + + mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH; + int f = open(target, O_WRONLY | O_CREAT | O_TRUNC, mode); + + if (f == -1){ +#ifdef STANDALONE + fprintf(stderr, "open error: %s\n", target); +#endif + return; + } + + archive_read_data_into_fd(a, f); + close(f); +} + +bool type1_read_file_into_buf(struct appimage_handler* handler, void* data, char** buffer, unsigned long* buf_size) { + (void) data; + + struct archive* a = handler->cache; + + struct archive_entry* entry = data; + + int64_t file_size = archive_entry_size(entry); + + char* new_buffer = (char*) malloc(sizeof(char) * file_size); + + if (new_buffer == NULL) { +#ifdef STANDALONE + fprintf(stderr, "failed to allocate enough memory for buffer (required: %ul bytes)\n", file_size); +#endif + return false; + } + + if (archive_read_data(a, new_buffer, (size_t) file_size) < 0) { +#ifdef STANDALONE + fprintf(stderr, "failed to read data into buffer: %s\n", archive_error_string(a)); +#endif + free(new_buffer); + return false; + } + + *buffer = new_buffer; + *buf_size = (unsigned long) file_size; + return true; +} + +char* type1_get_file_link(struct appimage_handler* handler, void* entry_ptr) { + struct archive_entry* entry = entry_ptr; + + const char* link_path = archive_entry_symlink(entry) ?: archive_entry_hardlink(entry); + + if (link_path) { + char* filename = replace_str(link_path, "./", ""); + return filename; + } + + return NULL; +} + +appimage_handler appimage_type_1_create_handler() { + appimage_handler h; + h.traverse = type1_traverse; + h.get_file_name = type1_get_file_name; + h.extract_file = type1_extract_file; + h.get_file_link = type1_get_file_link; + h.read_file_into_new_buffer = type1_read_file_into_buf; + h.type = 1; + + return h; +} + +bool match_type_1_magic_bytes(const char* buffer) { + return buffer[0] == 0x41 && buffer[1] == 0x49 && buffer[2] == 0x01; +} + +bool is_iso_9660_file(const char* path) { + /* Implementation of the signature matches expressed at https://www.garykessler.net/library/file_sigs.html + * Signature: 43 44 30 30 31 = "CD001" + * ISO ISO-9660 CD Disc Image + * This signature usually occurs at byte offset 32769 (0x8001), + * 34817 (0x8801), or 36865 (0x9001). + * More information can be found at MacTech or at ECMA. + */ + + bool res = false; + FILE* f = fopen(path, "rt"); + if (f != NULL) { + char buffer[5] = {0}; + + int positions[] = {32769, 34817, 36865}; + const char signature[] = "CD001"; + for (int i = 0; i < 3 && !res; i++) { + int fseekRes = fseek(f, positions[i], SEEK_SET); + if (!fseekRes) { + fread(buffer, 1, 5, f); + int strCmpRes = memcmp(signature, buffer, 5); + if (!strCmpRes) + res = true; + } + memset(buffer, 0, 5); + } + + fclose(f); + } + return res; +} diff --git a/src/libappimage/type1.h b/src/libappimage/type1.h new file mode 100644 index 0000000..7b29e65 --- /dev/null +++ b/src/libappimage/type1.h @@ -0,0 +1,28 @@ +#pragma once + +// local includes +#include "appimage_handler.h" + +appimage_handler appimage_type_1_create_handler(); + +/** + * According to the AppImage specification for type 1 files + * https://github.com/AppImage/AppImageSpec/blob/master/draft.md#type-1-image-format + * + * Match the buffer to 0x414901. + * @param buffer + * @return 1 if the values are the same, 0 otherwise + */ +bool match_type_1_magic_bytes(const char* buffer); + +/** + * Check for iso 9660 magic bytes. + * + * According to the AppImage specification for type 1 files + * https://github.com/AppImage/AppImageSpec/blob/master/draft.md#type-1-image-format + * the files must be valid ISO 9660 files. + * + * @param path path + * @return true if the file has the proper signature, false otherwise + */ +bool is_iso_9660_file(const char* path); \ No newline at end of file diff --git a/src/libappimage/type2.c b/src/libappimage/type2.c new file mode 100644 index 0000000..ce32112 --- /dev/null +++ b/src/libappimage/type2.c @@ -0,0 +1,250 @@ +// library includes +#include +#include +#include + +// local includes +#include "type2.h" + +void appimage_type2_open(appimage_handler* handler) { + if (is_handler_valid(handler) && !handler->is_open) { +#ifdef STANDALONE + fprintf(stderr, "Opening %s as Type 2 AppImage\n", handler->path); +#endif + // The offset at which a squashfs image is expected + ssize_t fs_offset = appimage_get_elf_size(handler->path); + + if (fs_offset < 0) { +#ifdef STANDALONE + fprintf(stderr, "get_elf_size error\n"); +#endif + handler->is_open = false; + handler->cache = NULL; + return; + } + + sqfs* fs = malloc(sizeof(sqfs)); + sqfs_err err = sqfs_open_image(fs, handler->path, (size_t) fs_offset); + if (err != SQFS_OK) { +#ifdef STANDALONE + fprintf(stderr, "sqfs_open_image error: %s\n", handler->path); +#endif + free(fs); + handler->is_open = false; + handler->cache = NULL; + } else { + handler->is_open = true; + handler->cache = fs; + } + } +} + +void appimage_type2_close(appimage_handler* handler) { + if (is_handler_valid(handler) && handler->is_open) { +#ifdef STANDALONE + fprintf(stderr, "Closing %s\n", handler->path); +#endif + + sqfs_destroy(handler->cache); + free(handler->cache); + + handler->is_open = false; + handler->cache = NULL; + } +} + +// forward declaration, see below +void appimage_type2_extract_symlink(sqfs* fs, sqfs_inode* inode, const char* target); +// TODO: get rid of this forward declaration +void squash_extract_inode_to_file(sqfs *fs, sqfs_inode *inode, const gchar *dest); + +void appimage_type2_extract_regular_file(sqfs* fs, sqfs_inode* inode, const char* target) { + mk_base_dir(target); + + // Read the file in chunks + squash_extract_inode_to_file(fs, inode, target); +} + +bool appimage_type2_resolve_symlink(sqfs* fs, sqfs_inode* inode) { + // no need to do anything if the passed inode is not a symlink + if (inode->base.inode_type != SQUASHFS_SYMLINK_TYPE) + return true; + + // read twice: once to populate size to be able to allocate the right amount of memory, then to populate the buffer + size_t size; + sqfs_readlink(fs, inode, NULL, &size); + + char buf[size]; + int ret = sqfs_readlink(fs, inode, buf, &size); + + if (ret != 0) { +#ifdef STANDALONE + fprintf(stderr, "WARNING: Symlink error."); +#endif + return false; + } + + sqfs_err err = sqfs_inode_get(fs, inode, fs->sb.root_inode); + if (err != SQFS_OK) { +#ifdef STANDALONE + fprintf(stderr, "WARNING: Unable to get the root inode. Error: %d", err); +#endif + return false; + } + + bool found = false; + err = sqfs_lookup_path(fs, inode, buf, &found); + if (err != SQFS_OK) { +#ifdef STANDALONE + fprintf(stderr, "WARNING: There was an error while trying to lookup a symblink. Error: %d", err); +#endif + return false; + } + + return true; +} + +bool appimage_type2_extract_file_following_symlinks(sqfs* fs, sqfs_inode* inode, const char* target) { + if (!appimage_type2_resolve_symlink(fs, inode)) { +#ifdef STANDALONE + fprintf(stderr, "ERROR: Failed to resolve symlink"); +#endif + return false; + } + + if (inode->base.inode_type != SQUASHFS_REG_TYPE && inode->base.inode_type != SQUASHFS_LREG_TYPE) { +#ifdef STANDALONE + fprintf(stderr, "WARNING: Unable to extract file of type %d", inode->base.inode_type); +#endif + return false; + } + + appimage_type2_extract_regular_file(fs, inode, target); + return true; +} + +void type2_traverse(appimage_handler* handler, traverse_cb command, void* command_data) { + appimage_type2_open(handler); + + if (handler->is_open && handler->cache != NULL) { + sqfs* fs = handler->cache; + sqfs_traverse trv; + sqfs_inode_id root_inode = sqfs_inode_root(fs); + sqfs_err err = sqfs_traverse_open(&trv, fs, root_inode); + if (err != SQFS_OK) { +#ifdef STANDALONE + fprintf(stderr, "sqfs_traverse_open error\n"); +#endif + } + while (sqfs_traverse_next(&trv, &err)) + command(handler, &trv, command_data); + + if (err) { +#ifdef STANDALONE + fprintf(stderr, "sqfs_traverse_next error\n"); +#endif + } + sqfs_traverse_close(&trv); + } + + appimage_type2_close(handler); +} + +char* type2_get_file_name(appimage_handler* handler, void* data) { + (void) handler; + sqfs_traverse* trv = data; + return strdup(trv->path); +} + +void type2_extract_file(appimage_handler* handler, void* data, const char* target) { + sqfs* fs = handler->cache; + sqfs_traverse* trv = data; + + sqfs_inode inode; + if (sqfs_inode_get(fs, &inode, trv->entry.inode)) { +#ifdef STANDALONE + fprintf(stderr, "sqfs_inode_get error\n"); +#endif + } + + appimage_type2_extract_file_following_symlinks(fs, &inode, target); +} + +bool type2_read_file_into_buf(struct appimage_handler* handler, void* traverse, char** buffer, unsigned long* buf_size) { + sqfs* fs = handler->cache; + sqfs_traverse* trv = traverse; + + sqfs_inode inode; + if (sqfs_inode_get(fs, &inode, trv->entry.inode)) { +#ifdef STANDALONE + fprintf(stderr, "sqfs_inode_get error\n"); +#endif + } + + // resolve symlink if possible + if (!appimage_type2_resolve_symlink(fs, &inode)) { +#ifdef STANDALONE + fprintf(stderr, "ERROR: Failed to resolve symlink"); +#endif + return false; + } + + if (inode.base.inode_type != SQUASHFS_REG_TYPE && inode.base.inode_type != SQUASHFS_LREG_TYPE) { +#ifdef STANDALONE + fprintf(stderr, "WARNING: Unable to extract file of type %d", inode->base.inode_type); +#endif + return false; + } + + uint64_t file_size = inode.xtra.reg.file_size; + + char* new_buffer = (char*) malloc(sizeof(char) * file_size); + + if (new_buffer == NULL) { +#ifdef STANDALONE + fprintf(stderr, "failed to allocate enough memory for buffer (required: %ul bytes)\n", file_size); +#endif + return false; + } + + if (sqfs_read_range(fs, &inode, 0, (sqfs_off_t*) &file_size, new_buffer) != SQFS_OK) { +#ifdef STANDALONE + fprintf(stderr, "failed to read data into buffer\n"); +#endif + free(new_buffer); + return false; + } + + *buffer = new_buffer; + *buf_size = file_size; + return true; +} + +char* type2_get_file_link(struct appimage_handler* handler, void* data) { + sqfs_traverse* trv = data; + + sqfs_inode inode; + if (!sqfs_inode_get(trv->fs, &inode, trv->entry.inode)) + return NULL; + + // read twice: once to populate size to be able to allocate the right amount of memory, then to populate the buffer + size_t size; + sqfs_readlink(trv->fs, &inode, NULL, &size); + + char* buf = malloc(sizeof(char) * size); + int ret = sqfs_readlink(trv->fs, &inode, buf, &size); + + return buf; +} + +appimage_handler appimage_type_2_create_handler() { + appimage_handler h; + h.traverse = type2_traverse; + h.get_file_name = type2_get_file_name; + h.get_file_link = type2_get_file_link; + h.extract_file = type2_extract_file; + h.read_file_into_new_buffer = type2_read_file_into_buf; + h.type = 2; + + return h; +} diff --git a/src/libappimage/type2.h b/src/libappimage/type2.h new file mode 100644 index 0000000..bee032b --- /dev/null +++ b/src/libappimage/type2.h @@ -0,0 +1,6 @@ +#pragma once + +// local includes +#include "appimage_handler.h" + +appimage_handler appimage_type_2_create_handler(); diff --git a/src/libappimage_hashlib/CMakeLists.txt b/src/libappimage_hashlib/CMakeLists.txt new file mode 100644 index 0000000..25006b3 --- /dev/null +++ b/src/libappimage_hashlib/CMakeLists.txt @@ -0,0 +1,17 @@ +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +set(public_header ${CMAKE_CURRENT_SOURCE_DIR}/include/hashlib.h) + +add_library(libappimage_hashlib STATIC md5.c ${public_header}) +set_target_properties(libappimage_hashlib PROPERTIES PREFIX "") +target_include_directories(libappimage_hashlib + PUBLIC $ +) + +# install libappimage +install(TARGETS libappimage_hashlib + EXPORT libappimageTargets + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT libappimage + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT libappimage + PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/appimage COMPONENT libappimage-dev +) diff --git a/src/libappimage_hashlib/include/hashlib.h b/src/libappimage_hashlib/include/hashlib.h new file mode 100644 index 0000000..9801b78 --- /dev/null +++ b/src/libappimage_hashlib/include/hashlib.h @@ -0,0 +1,4 @@ +#pragma once + +// include implementations +#include "md5.h" diff --git a/src/libappimage_hashlib/include/md5.h b/src/libappimage_hashlib/include/md5.h new file mode 100644 index 0000000..ede1546 --- /dev/null +++ b/src/libappimage_hashlib/include/md5.h @@ -0,0 +1,34 @@ +#pragma once + + +#include +#include + +typedef struct { + uint32_t lo; + uint32_t hi; + uint32_t a; + uint32_t b; + uint32_t c; + uint32_t d; + uint8_t buffer[64]; + uint32_t block[16]; +} Md5Context; + +#define MD5_HASH_SIZE (128 / 8) + +typedef struct { + uint8_t bytes[MD5_HASH_SIZE]; +} MD5_HASH; + +// initialize new context +void Md5Initialise(Md5Context* ctx); + +// add data to the context +void Md5Update(Md5Context* ctx, void const* buf, uint32_t bufSize); + +// calculate final digest from context +void Md5Finalise(Md5Context* ctx, MD5_HASH* digest); + +// create new context, add data from buffer to it, and calculate digest +void Md5Calculate(void const* Buffer, uint32_t BufferSize, MD5_HASH* Digest); diff --git a/src/libappimage_hashlib/md5.c b/src/libappimage_hashlib/md5.c new file mode 100644 index 0000000..bb3cf18 --- /dev/null +++ b/src/libappimage_hashlib/md5.c @@ -0,0 +1,335 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// WjCryptLib_Md5 +// +// Implementation of MD5 hash function. Originally written by Alexander Peslyak. Modified by WaterJuice retaining +// Public Domain license. +// +// This is free and unencumbered software released into the public domain - June 2013 waterjuice.org +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// IMPORTS +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#include "md5.h" +#include + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// INTERNAL FUNCTIONS +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// F, G, H, I +// +// The basic MD5 functions. F and G are optimised compared to their RFC 1321 definitions for architectures that lack +// an AND-NOT instruction, just like in Colin Plumb's implementation. +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#define F( x, y, z ) ( (z) ^ ((x) & ((y) ^ (z))) ) +#define G( x, y, z ) ( (y) ^ ((z) & ((x) ^ (y))) ) +#define H( x, y, z ) ( (x) ^ (y) ^ (z) ) +#define I( x, y, z ) ( (y) ^ ((x) | ~(z)) ) + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// STEP +// +// The MD5 transformation for all four rounds. +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#define STEP( f, a, b, c, d, x, t, s ) \ + (a) += f((b), (c), (d)) + (x) + (t); \ + (a) = (((a) << (s)) | (((a) & 0xffffffff) >> (32 - (s)))); \ + (a) += (b); + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// TransformFunction +// +// This processes one or more 64-byte data blocks, but does NOT update the bit counters. There are no alignment +// requirements. +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +static +void* +TransformFunction + ( + Md5Context* ctx, + void const* data, + uintmax_t size + ) +{ + uint8_t* ptr; + uint32_t a; + uint32_t b; + uint32_t c; + uint32_t d; + uint32_t saved_a; + uint32_t saved_b; + uint32_t saved_c; + uint32_t saved_d; + + #define GET(n) (ctx->block[(n)]) + #define SET(n) (ctx->block[(n)] = \ + ((uint32_t)ptr[(n)*4 + 0] << 0 ) \ + | ((uint32_t)ptr[(n)*4 + 1] << 8 ) \ + | ((uint32_t)ptr[(n)*4 + 2] << 16) \ + | ((uint32_t)ptr[(n)*4 + 3] << 24) ) + + ptr = (uint8_t*)data; + + a = ctx->a; + b = ctx->b; + c = ctx->c; + d = ctx->d; + + do + { + saved_a = a; + saved_b = b; + saved_c = c; + saved_d = d; + + // Round 1 + STEP( F, a, b, c, d, SET(0), 0xd76aa478, 7 ) + STEP( F, d, a, b, c, SET(1), 0xe8c7b756, 12 ) + STEP( F, c, d, a, b, SET(2), 0x242070db, 17 ) + STEP( F, b, c, d, a, SET(3), 0xc1bdceee, 22 ) + STEP( F, a, b, c, d, SET(4), 0xf57c0faf, 7 ) + STEP( F, d, a, b, c, SET(5), 0x4787c62a, 12 ) + STEP( F, c, d, a, b, SET(6), 0xa8304613, 17 ) + STEP( F, b, c, d, a, SET(7), 0xfd469501, 22 ) + STEP( F, a, b, c, d, SET(8 ), 0x698098d8, 7 ) + STEP( F, d, a, b, c, SET(9 ), 0x8b44f7af, 12 ) + STEP( F, c, d, a, b, SET(10 ), 0xffff5bb1, 17 ) + STEP( F, b, c, d, a, SET(11 ), 0x895cd7be, 22 ) + STEP( F, a, b, c, d, SET(12 ), 0x6b901122, 7 ) + STEP( F, d, a, b, c, SET(13 ), 0xfd987193, 12 ) + STEP( F, c, d, a, b, SET(14 ), 0xa679438e, 17 ) + STEP( F, b, c, d, a, SET(15 ), 0x49b40821, 22 ) + + // Round 2 + STEP( G, a, b, c, d, GET(1), 0xf61e2562, 5 ) + STEP( G, d, a, b, c, GET(6), 0xc040b340, 9 ) + STEP( G, c, d, a, b, GET(11), 0x265e5a51, 14 ) + STEP( G, b, c, d, a, GET(0), 0xe9b6c7aa, 20 ) + STEP( G, a, b, c, d, GET(5), 0xd62f105d, 5 ) + STEP( G, d, a, b, c, GET(10), 0x02441453, 9 ) + STEP( G, c, d, a, b, GET(15), 0xd8a1e681, 14 ) + STEP( G, b, c, d, a, GET(4), 0xe7d3fbc8, 20 ) + STEP( G, a, b, c, d, GET(9), 0x21e1cde6, 5 ) + STEP( G, d, a, b, c, GET(14), 0xc33707d6, 9 ) + STEP( G, c, d, a, b, GET(3), 0xf4d50d87, 14 ) + STEP( G, b, c, d, a, GET(8), 0x455a14ed, 20 ) + STEP( G, a, b, c, d, GET(13), 0xa9e3e905, 5 ) + STEP( G, d, a, b, c, GET(2), 0xfcefa3f8, 9 ) + STEP( G, c, d, a, b, GET(7), 0x676f02d9, 14 ) + STEP( G, b, c, d, a, GET(12), 0x8d2a4c8a, 20 ) + + // Round 3 + STEP( H, a, b, c, d, GET(5), 0xfffa3942, 4 ) + STEP( H, d, a, b, c, GET(8), 0x8771f681, 11 ) + STEP( H, c, d, a, b, GET(11), 0x6d9d6122, 16 ) + STEP( H, b, c, d, a, GET(14), 0xfde5380c, 23 ) + STEP( H, a, b, c, d, GET(1), 0xa4beea44, 4 ) + STEP( H, d, a, b, c, GET(4), 0x4bdecfa9, 11 ) + STEP( H, c, d, a, b, GET(7), 0xf6bb4b60, 16 ) + STEP( H, b, c, d, a, GET(10), 0xbebfbc70, 23 ) + STEP( H, a, b, c, d, GET(13), 0x289b7ec6, 4 ) + STEP( H, d, a, b, c, GET(0), 0xeaa127fa, 11 ) + STEP( H, c, d, a, b, GET(3), 0xd4ef3085, 16 ) + STEP( H, b, c, d, a, GET(6), 0x04881d05, 23 ) + STEP( H, a, b, c, d, GET(9), 0xd9d4d039, 4 ) + STEP( H, d, a, b, c, GET(12), 0xe6db99e5, 11 ) + STEP( H, c, d, a, b, GET(15), 0x1fa27cf8, 16 ) + STEP( H, b, c, d, a, GET(2), 0xc4ac5665, 23 ) + + // Round 4 + STEP( I, a, b, c, d, GET(0), 0xf4292244, 6 ) + STEP( I, d, a, b, c, GET(7), 0x432aff97, 10 ) + STEP( I, c, d, a, b, GET(14), 0xab9423a7, 15 ) + STEP( I, b, c, d, a, GET(5), 0xfc93a039, 21 ) + STEP( I, a, b, c, d, GET(12), 0x655b59c3, 6 ) + STEP( I, d, a, b, c, GET(3), 0x8f0ccc92, 10 ) + STEP( I, c, d, a, b, GET(10), 0xffeff47d, 15 ) + STEP( I, b, c, d, a, GET(1), 0x85845dd1, 21 ) + STEP( I, a, b, c, d, GET(8), 0x6fa87e4f, 6 ) + STEP( I, d, a, b, c, GET(15), 0xfe2ce6e0, 10 ) + STEP( I, c, d, a, b, GET(6), 0xa3014314, 15 ) + STEP( I, b, c, d, a, GET(13), 0x4e0811a1, 21 ) + STEP( I, a, b, c, d, GET(4), 0xf7537e82, 6 ) + STEP( I, d, a, b, c, GET(11), 0xbd3af235, 10 ) + STEP( I, c, d, a, b, GET(2), 0x2ad7d2bb, 15 ) + STEP( I, b, c, d, a, GET(9), 0xeb86d391, 21 ) + + a += saved_a; + b += saved_b; + c += saved_c; + d += saved_d; + + ptr += 64; + } while( size -= 64 ); + + ctx->a = a; + ctx->b = b; + ctx->c = c; + ctx->d = d; + + #undef GET + #undef SET + + return ptr; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// EXPORTED FUNCTIONS +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Md5Initialise +// +// Initialises an MD5 Context. Use this to initialise/reset a context. +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void +Md5Initialise + ( + Md5Context* Context // [out] + ) +{ + Context->a = 0x67452301; + Context->b = 0xefcdab89; + Context->c = 0x98badcfe; + Context->d = 0x10325476; + + Context->lo = 0; + Context->hi = 0; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Md5Update +// +// Adds data to the MD5 context. This will process the data and update the internal state of the context. Keep on +// calling this function until all the data has been added. Then call Md5Finalise to calculate the hash. +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void +Md5Update + ( + Md5Context* Context, // [in out] + void const* Buffer, // [in] + uint32_t BufferSize // [in] + ) +{ + uint32_t saved_lo; + uint32_t used; + uint32_t free; + + saved_lo = Context->lo; + if( (Context->lo = (saved_lo + BufferSize) & 0x1fffffff) < saved_lo ) + { + Context->hi++; + } + Context->hi += (uint32_t)( BufferSize >> 29 ); + + used = saved_lo & 0x3f; + + if( used ) + { + free = 64 - used; + + if( BufferSize < free ) + { + memcpy( &Context->buffer[used], Buffer, BufferSize ); + return; + } + + memcpy( &Context->buffer[used], Buffer, free ); + Buffer = (uint8_t*)Buffer + free; + BufferSize -= free; + TransformFunction(Context, Context->buffer, 64); + } + + if( BufferSize >= 64 ) + { + Buffer = TransformFunction( Context, Buffer, BufferSize & ~(unsigned long)0x3f ); + BufferSize &= 0x3f; + } + + memcpy( Context->buffer, Buffer, BufferSize ); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Md5Finalise +// +// Performs the final calculation of the hash and returns the digest (16 byte buffer containing 128bit hash). After +// calling this, Md5Initialised must be used to reuse the context. +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void +Md5Finalise + ( + Md5Context* Context, // [in out] + MD5_HASH* Digest // [in] + ) +{ + uint32_t used; + uint32_t free; + + used = Context->lo & 0x3f; + + Context->buffer[used++] = 0x80; + + free = 64 - used; + + if(free < 8) + { + memset( &Context->buffer[used], 0, free ); + TransformFunction( Context, Context->buffer, 64 ); + used = 0; + free = 64; + } + + memset( &Context->buffer[used], 0, free - 8 ); + + Context->lo <<= 3; + Context->buffer[56] = (uint8_t)( Context->lo ); + Context->buffer[57] = (uint8_t)( Context->lo >> 8 ); + Context->buffer[58] = (uint8_t)( Context->lo >> 16 ); + Context->buffer[59] = (uint8_t)( Context->lo >> 24 ); + Context->buffer[60] = (uint8_t)( Context->hi ); + Context->buffer[61] = (uint8_t)( Context->hi >> 8 ); + Context->buffer[62] = (uint8_t)( Context->hi >> 16 ); + Context->buffer[63] = (uint8_t)( Context->hi >> 24 ); + + TransformFunction( Context, Context->buffer, 64 ); + + Digest->bytes[0] = (uint8_t)( Context->a ); + Digest->bytes[1] = (uint8_t)( Context->a >> 8 ); + Digest->bytes[2] = (uint8_t)( Context->a >> 16 ); + Digest->bytes[3] = (uint8_t)( Context->a >> 24 ); + Digest->bytes[4] = (uint8_t)( Context->b ); + Digest->bytes[5] = (uint8_t)( Context->b >> 8 ); + Digest->bytes[6] = (uint8_t)( Context->b >> 16 ); + Digest->bytes[7] = (uint8_t)( Context->b >> 24 ); + Digest->bytes[8] = (uint8_t)( Context->c ); + Digest->bytes[9] = (uint8_t)( Context->c >> 8 ); + Digest->bytes[10] = (uint8_t)( Context->c >> 16 ); + Digest->bytes[11] = (uint8_t)( Context->c >> 24 ); + Digest->bytes[12] = (uint8_t)( Context->d ); + Digest->bytes[13] = (uint8_t)( Context->d >> 8 ); + Digest->bytes[14] = (uint8_t)( Context->d >> 16 ); + Digest->bytes[15] = (uint8_t)( Context->d >> 24 ); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Md5Calculate +// +// Combines Md5Initialise, Md5Update, and Md5Finalise into one function. Calculates the MD5 hash of the buffer. +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void +Md5Calculate + ( + void const* Buffer, // [in] + uint32_t BufferSize, // [in] + MD5_HASH* Digest // [in] + ) +{ + Md5Context context; + + Md5Initialise( &context ); + Md5Update( &context, Buffer, BufferSize ); + Md5Finalise( &context, Digest ); +} diff --git a/src/libappimage_shared/CMakeLists.txt b/src/libappimage_shared/CMakeLists.txt new file mode 100644 index 0000000..ef7238b --- /dev/null +++ b/src/libappimage_shared/CMakeLists.txt @@ -0,0 +1,27 @@ +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +set(libappimage_shared_public_header ${PROJECT_SOURCE_DIR}/include/appimage/appimage_shared.h) + +add_library(libappimage_shared STATIC + ${libappimage_shared_public_header} + elf.c + hexlify.c + light_byteswap.h + light_elf.h + digest.c +) +set_target_properties(libappimage_shared PROPERTIES PREFIX "") +target_include_directories(libappimage_shared PUBLIC + $ + $ +) +set_property(TARGET libappimage_shared PROPERTY PUBLIC_HEADER ${libappimage_shared_public_header}) +target_link_libraries(libappimage_shared PRIVATE libappimage_hashlib) + +# install libappimage +install(TARGETS libappimage_shared + EXPORT libappimageTargets + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT libappimage + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT libappimage + PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/appimage COMPONENT libappimage-dev +) diff --git a/src/libappimage_shared/digest.c b/src/libappimage_shared/digest.c new file mode 100644 index 0000000..a9294b8 --- /dev/null +++ b/src/libappimage_shared/digest.c @@ -0,0 +1,139 @@ +#include +#include +#include + +#include +#include + +bool appimage_type2_digest_md5(const char* path, char* digest) { + // skip digest, signature and key sections in digest calculation + unsigned long digest_md5_offset = 0, digest_md5_length = 0; + if (!appimage_get_elf_section_offset_and_length(path, ".digest_md5", &digest_md5_offset, &digest_md5_length)) + return false; + + unsigned long signature_offset = 0, signature_length = 0; + if (!appimage_get_elf_section_offset_and_length(path, ".sha256_sig", &signature_offset, &signature_length)) + return false; + + unsigned long sig_key_offset = 0, sig_key_length = 0; + if (!appimage_get_elf_section_offset_and_length(path, ".sig_key", &sig_key_offset, &sig_key_length)) + return false; + + Md5Context md5_context; + Md5Initialise(&md5_context); + + // read file in chunks + static const int chunk_size = 4096; + + FILE *fp = fopen(path, "r"); + + // determine file size + fseek(fp, 0L, SEEK_END); + const long file_size = ftell(fp); + rewind(fp); + + long bytes_left = file_size; + + // if a section spans over more than a single chunk, we need emulate null bytes in the following chunks + ssize_t bytes_skip_following_chunks = 0; + + while (bytes_left > 0) { + char buffer[chunk_size]; + + long current_position = ftell(fp); + + ssize_t bytes_left_this_chunk = chunk_size; + + // first, check whether there's bytes left that need to be skipped + if (bytes_skip_following_chunks > 0) { + ssize_t bytes_skip_this_chunk = (bytes_skip_following_chunks % chunk_size == 0) ? chunk_size : (bytes_skip_following_chunks % chunk_size); + bytes_left_this_chunk -= bytes_skip_this_chunk; + + // we could just set it to 0 here, but it makes more sense to use -= for debugging + bytes_skip_following_chunks -= bytes_skip_this_chunk; + + // make sure to skip these bytes in the file + fseek(fp, bytes_skip_this_chunk, SEEK_CUR); + } + + // check whether there's a section in this chunk that we need to skip + if (digest_md5_offset != 0 && digest_md5_length != 0 && digest_md5_offset - current_position > 0 && digest_md5_offset - current_position < chunk_size) { + ssize_t begin_of_section = (digest_md5_offset - current_position) % chunk_size; + // read chunk before section + fread(buffer, sizeof(char), (size_t) begin_of_section, fp); + + bytes_left_this_chunk -= begin_of_section; + bytes_left_this_chunk -= digest_md5_length; + + // if bytes_left is now < 0, the section exceeds the current chunk + // this amount of bytes needs to be skipped in the future sections + if (bytes_left_this_chunk < 0) { + bytes_skip_following_chunks = (size_t) (-1 * bytes_left_this_chunk); + bytes_left_this_chunk = 0; + } + + // if there's bytes left to read, we need to seek the difference between chunk's end and bytes_left + fseek(fp, (chunk_size - bytes_left_this_chunk - begin_of_section), SEEK_CUR); + } + + // check whether there's a section in this chunk that we need to skip + if (signature_offset != 0 && signature_length != 0 && signature_offset - current_position > 0 && signature_offset - current_position < chunk_size) { + ssize_t begin_of_section = (signature_offset - current_position) % chunk_size; + // read chunk before section + fread(buffer, sizeof(char), (size_t) begin_of_section, fp); + + bytes_left_this_chunk -= begin_of_section; + bytes_left_this_chunk -= signature_length; + + // if bytes_left is now < 0, the section exceeds the current chunk + // this amount of bytes needs to be skipped in the future sections + if (bytes_left_this_chunk < 0) { + bytes_skip_following_chunks = (size_t) (-1 * bytes_left_this_chunk); + bytes_left_this_chunk = 0; + } + + // if there's bytes left to read, we need to seek the difference between chunk's end and bytes_left + fseek(fp, (chunk_size - bytes_left_this_chunk - begin_of_section), SEEK_CUR); + } + + // check whether there's a section in this chunk that we need to skip + if (sig_key_offset != 0 && sig_key_length != 0 && sig_key_offset - current_position > 0 && sig_key_offset - current_position < chunk_size) { + ssize_t begin_of_section = (sig_key_offset - current_position) % chunk_size; + // read chunk before section + fread(buffer, sizeof(char), (size_t) begin_of_section, fp); + + bytes_left_this_chunk -= begin_of_section; + bytes_left_this_chunk -= sig_key_length; + + // if bytes_left is now < 0, the section exceeds the current chunk + // this amount of bytes needs to be skipped in the future sections + if (bytes_left_this_chunk < 0) { + bytes_skip_following_chunks = (size_t) (-1 * bytes_left_this_chunk); + bytes_left_this_chunk = 0; + } + + // if there's bytes left to read, we need to seek the difference between chunk's end and bytes_left + fseek(fp, (chunk_size - bytes_left_this_chunk - begin_of_section), SEEK_CUR); + } + + // check whether we're done already + if (bytes_left_this_chunk > 0) { + // read data from file into buffer with the correct offset in case bytes have to be skipped + fread(buffer + (chunk_size - bytes_left_this_chunk), sizeof(char), (size_t) bytes_left_this_chunk, fp); + } + + // feed buffer into checksum calculation + Md5Update(&md5_context, buffer, chunk_size); + + bytes_left -= chunk_size; + } + + MD5_HASH checksum; + Md5Finalise(&md5_context, &checksum); + + memcpy(digest, (const char*) checksum.bytes, 16); + + fclose(fp); + + return true; +} diff --git a/src/libappimage_shared/elf.c b/src/libappimage_shared/elf.c new file mode 100644 index 0000000..d5a0594 --- /dev/null +++ b/src/libappimage_shared/elf.c @@ -0,0 +1,252 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "light_elf.h" +#include "light_byteswap.h" + + +typedef Elf32_Nhdr Elf_Nhdr; + +static char *fname; +static Elf64_Ehdr ehdr; + +#if __BYTE_ORDER == __LITTLE_ENDIAN +#define ELFDATANATIVE ELFDATA2LSB +#elif __BYTE_ORDER == __BIG_ENDIAN +#define ELFDATANATIVE ELFDATA2MSB +#else +#error "Unknown machine endian" +#endif + +static uint16_t file16_to_cpu(uint16_t val) +{ + if (ehdr.e_ident[EI_DATA] != ELFDATANATIVE) + val = bswap_16(val); + return val; +} + +static uint32_t file32_to_cpu(uint32_t val) +{ + if (ehdr.e_ident[EI_DATA] != ELFDATANATIVE) + val = bswap_32(val); + return val; +} + +static uint64_t file64_to_cpu(uint64_t val) +{ + if (ehdr.e_ident[EI_DATA] != ELFDATANATIVE) + val = bswap_64(val); + return val; +} + +static off_t read_elf32(FILE* fd) +{ + Elf32_Ehdr ehdr32; + Elf32_Shdr shdr32; + off_t last_shdr_offset; + ssize_t ret; + off_t sht_end, last_section_end; + + fseeko(fd, 0, SEEK_SET); + ret = fread(&ehdr32, 1, sizeof(ehdr32), fd); + if (ret < 0 || (size_t)ret != sizeof(ehdr32)) { + fprintf(stderr, "Read of ELF header from %s failed: %s\n", + fname, strerror(errno)); + return -1; + } + + ehdr.e_shoff = file32_to_cpu(ehdr32.e_shoff); + ehdr.e_shentsize = file16_to_cpu(ehdr32.e_shentsize); + ehdr.e_shnum = file16_to_cpu(ehdr32.e_shnum); + + last_shdr_offset = ehdr.e_shoff + (ehdr.e_shentsize * (ehdr.e_shnum - 1)); + fseeko(fd, last_shdr_offset, SEEK_SET); + ret = fread(&shdr32, 1, sizeof(shdr32), fd); + if (ret < 0 || (size_t)ret != sizeof(shdr32)) { + fprintf(stderr, "Read of ELF section header from %s failed: %s\n", + fname, strerror(errno)); + return -1; + } + + /* ELF ends either with the table of section headers (SHT) or with a section. */ + sht_end = ehdr.e_shoff + (ehdr.e_shentsize * ehdr.e_shnum); + last_section_end = file64_to_cpu(shdr32.sh_offset) + file64_to_cpu(shdr32.sh_size); + return sht_end > last_section_end ? sht_end : last_section_end; +} + +static off_t read_elf64(FILE* fd) +{ + Elf64_Ehdr ehdr64; + Elf64_Shdr shdr64; + off_t last_shdr_offset; + off_t ret; + off_t sht_end, last_section_end; + + fseeko(fd, 0, SEEK_SET); + ret = fread(&ehdr64, 1, sizeof(ehdr64), fd); + if (ret < 0 || (size_t)ret != sizeof(ehdr64)) { + fprintf(stderr, "Read of ELF header from %s failed: %s\n", + fname, strerror(errno)); + return -1; + } + + ehdr.e_shoff = file64_to_cpu(ehdr64.e_shoff); + ehdr.e_shentsize = file16_to_cpu(ehdr64.e_shentsize); + ehdr.e_shnum = file16_to_cpu(ehdr64.e_shnum); + + last_shdr_offset = ehdr.e_shoff + (ehdr.e_shentsize * (ehdr.e_shnum - 1)); + fseeko(fd, last_shdr_offset, SEEK_SET); + ret = fread(&shdr64, 1, sizeof(shdr64), fd); + if (ret < 0 || ret != sizeof(shdr64)) { + fprintf(stderr, "Read of ELF section header from %s failed: %s\n", + fname, strerror(errno)); + return -1; + } + + /* ELF ends either with the table of section headers (SHT) or with a section. */ + sht_end = ehdr.e_shoff + (ehdr.e_shentsize * ehdr.e_shnum); + last_section_end = file64_to_cpu(shdr64.sh_offset) + file64_to_cpu(shdr64.sh_size); + return sht_end > last_section_end ? sht_end : last_section_end; +} + +ssize_t appimage_get_elf_size(const char* fname) { + off_t ret; + FILE* fd = NULL; + off_t size = -1; + + fd = fopen(fname, "rb"); + if (fd == NULL) { + fprintf(stderr, "Cannot open %s: %s\n", + fname, strerror(errno)); + return -1; + } + ret = fread(ehdr.e_ident, 1, EI_NIDENT, fd); + if (ret != EI_NIDENT) { + fprintf(stderr, "Read of e_ident from %s failed: %s\n", + fname, strerror(errno)); + return -1; + } + if ((ehdr.e_ident[EI_DATA] != ELFDATA2LSB) && + (ehdr.e_ident[EI_DATA] != ELFDATA2MSB)) { + fprintf(stderr, "Unknown ELF data order %u\n", + ehdr.e_ident[EI_DATA]); + return -1; + } + if (ehdr.e_ident[EI_CLASS] == ELFCLASS32) { + size = read_elf32(fd); + } else if (ehdr.e_ident[EI_CLASS] == ELFCLASS64) { + size = read_elf64(fd); + } else { + fprintf(stderr, "Unknown ELF class %u\n", ehdr.e_ident[EI_CLASS]); + return -1; + } + + fclose(fd); + return size; +} + +/* Return the offset, and the length of an ELF section with a given name in a given ELF file */ +bool appimage_get_elf_section_offset_and_length(const char* fname, const char* section_name, unsigned long* offset, unsigned long* length) { + uint8_t* data; + int i; + int fd = open(fname, O_RDONLY); + size_t map_size = (size_t) lseek(fd, 0, SEEK_END); + + data = mmap(NULL, map_size, PROT_READ, MAP_SHARED, fd, 0); + close(fd); + + // this trick works as both 32 and 64 bit ELF files start with the e_ident[EI_NINDENT] section + unsigned char class = data[EI_CLASS]; + + if (class == ELFCLASS32) { + Elf32_Ehdr* elf; + Elf32_Shdr* shdr; + + elf = (Elf32_Ehdr*) data; + shdr = (Elf32_Shdr*) (data + ((Elf32_Ehdr*) elf)->e_shoff); + + char* strTab = (char*) (data + shdr[elf->e_shstrndx].sh_offset); + for (i = 0; i < elf->e_shnum; i++) { + if (strcmp(&strTab[shdr[i].sh_name], section_name) == 0) { + *offset = shdr[i].sh_offset; + *length = shdr[i].sh_size; + } + } + } else if (class == ELFCLASS64) { + Elf64_Ehdr* elf; + Elf64_Shdr* shdr; + + elf = (Elf64_Ehdr*) data; + shdr = (Elf64_Shdr*) (data + elf->e_shoff); + + char* strTab = (char*) (data + shdr[elf->e_shstrndx].sh_offset); + for (i = 0; i < elf->e_shnum; i++) { + if (strcmp(&strTab[shdr[i].sh_name], section_name) == 0) { + *offset = shdr[i].sh_offset; + *length = shdr[i].sh_size; + } + } + } else { + fprintf(stderr, "Platforms other than 32-bit/64-bit are currently not supported!"); + munmap(data, map_size); + return false; + } + + munmap(data, map_size); + return true; +} + +char* read_file_offset_length(const char* fname, unsigned long offset, unsigned long length) { + FILE* f; + if ((f = fopen(fname, "r")) == NULL) { + return NULL; + } + + fseek(f, offset, SEEK_SET); + + char* buffer = calloc(length + 1, sizeof(char)); + fread(buffer, length, sizeof(char), f); + + fclose(f); + + return buffer; +} + +int appimage_print_hex(char* fname, unsigned long offset, unsigned long length) { + char* data; + if ((data = read_file_offset_length(fname, offset, length)) == NULL) { + return 1; + } + + for (long long k = 0; k < length && data[k] != '\0'; k++) { + printf("%x", data[k]); + } + + free(data); + + printf("\n"); + + return 0; +} + +int appimage_print_binary(char* fname, unsigned long offset, unsigned long length) { + char* data; + if ((data = read_file_offset_length(fname, offset, length)) == NULL) { + return 1; + } + + printf("%s\n", data); + + free(data); + + return 0; +} + diff --git a/src/libappimage_shared/hexlify.c b/src/libappimage_shared/hexlify.c new file mode 100644 index 0000000..113a595 --- /dev/null +++ b/src/libappimage_shared/hexlify.c @@ -0,0 +1,18 @@ +#include +#include +#include + +char* appimage_hexlify(const char* bytes, const size_t numBytes) { + // first of all, allocate the new string + // a hexadecimal representation works like "every byte will be represented by two chars" + // additionally, we need to null-terminate the string + char* hexlified = (char*) calloc((2 * numBytes + 1), sizeof(char)); + + for (size_t i = 0; i < numBytes; i++) { + char buffer[3]; + sprintf(buffer, "%02x", (unsigned char) bytes[i]); + strcat(hexlified, buffer); + } + + return hexlified; +} diff --git a/src/libappimage_shared/light_byteswap.h b/src/libappimage_shared/light_byteswap.h new file mode 100644 index 0000000..d0b98ba --- /dev/null +++ b/src/libappimage_shared/light_byteswap.h @@ -0,0 +1,13 @@ +#pragma once + +#define bswap_16(value) \ +((((value) & 0xff) << 8) | ((value) >> 8)) + +#define bswap_32(value) \ +(((uint32_t)bswap_16((uint16_t)((value) & 0xffff)) << 16) | \ +(uint32_t)bswap_16((uint16_t)((value) >> 16))) + +#define bswap_64(value) \ +(((uint64_t)bswap_32((uint32_t)((value) & 0xffffffff)) \ +<< 32) | \ +(uint64_t)bswap_32((uint32_t)((value) >> 32))) diff --git a/src/libappimage_shared/light_elf.h b/src/libappimage_shared/light_elf.h new file mode 100644 index 0000000..d6044a7 --- /dev/null +++ b/src/libappimage_shared/light_elf.h @@ -0,0 +1,119 @@ +/* + * + * Linux kernel + * Copyright (C) 2017 Linus Torvalds + * Modified work Copyright (C) 2017 @teras (https://github.com/teras) + * (Shortened version -- original work found here: + * https://github.com/torvalds/linux/blob/master/include/uapi/linux/elf.h) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef LIGHT_ELF_H +#define LIGHT_ELF_H + +#include + +__BEGIN_DECLS + +typedef uint16_t Elf32_Half; +typedef uint16_t Elf64_Half; +typedef uint32_t Elf32_Word; +typedef uint32_t Elf64_Word; +typedef uint64_t Elf64_Xword; +typedef uint32_t Elf32_Addr; +typedef uint64_t Elf64_Addr; +typedef uint32_t Elf32_Off; +typedef uint64_t Elf64_Off; + +#define EI_NIDENT 16 + +typedef struct elf32_hdr { + unsigned char e_ident[EI_NIDENT]; + Elf32_Half e_type; + Elf32_Half e_machine; + Elf32_Word e_version; + Elf32_Addr e_entry; /* Entry point */ + Elf32_Off e_phoff; + Elf32_Off e_shoff; + Elf32_Word e_flags; + Elf32_Half e_ehsize; + Elf32_Half e_phentsize; + Elf32_Half e_phnum; + Elf32_Half e_shentsize; + Elf32_Half e_shnum; + Elf32_Half e_shstrndx; +} Elf32_Ehdr; + +typedef struct elf64_hdr { + unsigned char e_ident[EI_NIDENT]; /* ELF "magic number" */ + Elf64_Half e_type; + Elf64_Half e_machine; + Elf64_Word e_version; + Elf64_Addr e_entry; /* Entry point virtual address */ + Elf64_Off e_phoff; /* Program header table file offset */ + Elf64_Off e_shoff; /* Section header table file offset */ + Elf64_Word e_flags; + Elf64_Half e_ehsize; + Elf64_Half e_phentsize; + Elf64_Half e_phnum; + Elf64_Half e_shentsize; + Elf64_Half e_shnum; + Elf64_Half e_shstrndx; +} Elf64_Ehdr; + +typedef struct elf32_shdr { + Elf32_Word sh_name; + Elf32_Word sh_type; + Elf32_Word sh_flags; + Elf32_Addr sh_addr; + Elf32_Off sh_offset; + Elf32_Word sh_size; + Elf32_Word sh_link; + Elf32_Word sh_info; + Elf32_Word sh_addralign; + Elf32_Word sh_entsize; +} Elf32_Shdr; + +typedef struct elf64_shdr { + Elf64_Word sh_name; /* Section name, index in string tbl */ + Elf64_Word sh_type; /* Type of section */ + Elf64_Xword sh_flags; /* Miscellaneous section attributes */ + Elf64_Addr sh_addr; /* Section virtual addr at execution */ + Elf64_Off sh_offset; /* Section file offset */ + Elf64_Xword sh_size; /* Size of section in bytes */ + Elf64_Word sh_link; /* Index of another section */ + Elf64_Word sh_info; /* Additional section information */ + Elf64_Xword sh_addralign; /* Section alignment */ + Elf64_Xword sh_entsize; /* Entry size if section holds table */ +} Elf64_Shdr; + +/* Note header in a PT_NOTE section */ +typedef struct elf32_note { + Elf32_Word n_namesz; /* Name size */ + Elf32_Word n_descsz; /* Content size */ + Elf32_Word n_type; /* Content type */ +} Elf32_Nhdr; + +#define ELFCLASS32 1 +#define ELFDATA2LSB 1 +#define ELFDATA2MSB 2 +#define ELFCLASS64 2 +#define EI_CLASS 4 +#define EI_DATA 5 + +__END_DECLS + +#endif /* elf.h */ diff --git a/src/patches/patch-squashfuse.sh.in b/src/patches/patch-squashfuse.sh.in new file mode 100755 index 0000000..bd3452f --- /dev/null +++ b/src/patches/patch-squashfuse.sh.in @@ -0,0 +1,8 @@ +#! /bin/bash + +git checkout ll.c Makefile.am fuseprivate.c fuseprivate.h hl.c ll.h ll_inode.c nonstd-daemon.c + +patch -p1 < @PROJECT_SOURCE_DIR@/src/patches/squashfuse.patch +patch -p1 < @PROJECT_SOURCE_DIR@/src/patches/squashfuse_dlopen.patch + +cp -v @PROJECT_SOURCE_DIR@/src/patches/squashfuse_dlopen.c @PROJECT_SOURCE_DIR@/src/patches/squashfuse_dlopen.h . diff --git a/src/patches/squashfuse.patch b/src/patches/squashfuse.patch new file mode 100644 index 0000000..db94d2f --- /dev/null +++ b/src/patches/squashfuse.patch @@ -0,0 +1,57 @@ +diff --git a/Makefile.am b/Makefile.am +index f0d7cde..70c4aa0 100644 +--- a/Makefile.am ++++ b/Makefile.am +@@ -14,6 +14,7 @@ bin_PROGRAMS = + noinst_PROGRAMS = + + noinst_LTLIBRARIES = libsquashfuse.la ++noinst_LTLIBRARIES += libsquashfuse_ll.la + + # Main library: libsquashfuse + libsquashfuse_la_SOURCES = swap.c cache.c table.c dir.c file.c fs.c \ +@@ -46,10 +47,9 @@ endif + + # Low-level squashfuse_ll, if supported + if SQ_WANT_LOWLEVEL +-bin_PROGRAMS += squashfuse_ll +-squashfuse_ll_SOURCES = ll.c ll_inode.c nonstd-daemon.c ll.h +-squashfuse_ll_CPPFLAGS = $(FUSE_CPPFLAGS) +-squashfuse_ll_LDADD = libsquashfuse.la libfuseprivate.la $(COMPRESSION_LIBS) \ ++libsquashfuse_ll_la_SOURCES = ll.c ll_inode.c nonstd-daemon.c ll.h ++libsquashfuse_ll_la_CPPFLAGS = $(FUSE_CPPFLAGS) ++libsquashfuse_ll_la_LIBADD = libsquashfuse.la libfuseprivate.la $(COMPRESSION_LIBS) \ + $(FUSE_LIBS) + + noinst_LTLIBRARIES += libfuseprivate.la +diff --git a/ll.c b/ll.c +index a2c7902..8fcb3f4 100644 +--- a/ll.c ++++ b/ll.c +@@ -390,7 +390,7 @@ static sqfs_ll *sqfs_ll_open(const char *path, size_t offset) { + return NULL; + } + +-int main(int argc, char *argv[]) { ++int fusefs_main(int argc, char *argv[], void (*mounted) (void)) { + struct fuse_args args; + sqfs_opts opts; + +@@ -451,6 +451,8 @@ int main(int argc, char *argv[]) { + if (sqfs_ll_daemonize(fg) != -1) { + if (fuse_set_signal_handlers(se) != -1) { + fuse_session_add_chan(se, ch.ch); ++ if (mounted) ++ mounted (); + /* FIXME: multithreading */ + err = fuse_session_loop(se); + fuse_remove_signal_handlers(se); +@@ -466,6 +468,8 @@ int main(int argc, char *argv[]) { + } + } + fuse_opt_free_args(&args); ++ if (mounted) ++ rmdir (mountpoint); + free(ll); + free(mountpoint); + diff --git a/src/patches/squashfuse_dlopen.c b/src/patches/squashfuse_dlopen.c new file mode 100644 index 0000000..e4a9d6a --- /dev/null +++ b/src/patches/squashfuse_dlopen.c @@ -0,0 +1,11 @@ +#include "squashfuse_dlopen.h" + +int have_libloaded = 0; + +const char *load_library_errmsg = + "AppImages require FUSE to run. \n" + "You might still be able to extract the contents of this AppImage \n" + "if you run it with the --appimage-extract option. \n" + "See https://github.com/AppImage/AppImageKit/wiki/FUSE \n" + "for more information\n"; + diff --git a/src/patches/squashfuse_dlopen.h b/src/patches/squashfuse_dlopen.h new file mode 100644 index 0000000..da3c34f --- /dev/null +++ b/src/patches/squashfuse_dlopen.h @@ -0,0 +1,262 @@ +#ifndef SQFS_DLOPEN_H +#define SQFS_DLOPEN_H + +//#define ENABLE_DLOPEN + +#ifdef ENABLE_DLOPEN + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +/*** dlopen() stuff ***/ + +#define LIBNAME "libfuse.so.2" + +void *libhandle; +int have_libloaded; +const char *load_library_errmsg; + +#define LOAD_LIBRARY \ +if (have_libloaded != 1) { \ + if (!(libhandle = dlopen(LIBNAME, RTLD_LAZY))) { \ + fprintf(stderr, "dlopen(): error loading " LIBNAME "\n\n%s", load_library_errmsg ); \ + exit(1); \ + } else { \ + have_libloaded = 1; \ + } \ +} + +#define STRINGIFY(x) #x + +#define LOAD_SYMBOL(type,x,param) \ +type (*dl_##x) param; \ +*(void **) (&dl_##x) = dlsym(libhandle, STRINGIFY(x)); \ +if (dlerror()) { \ + fprintf(stderr, "dlsym(): error loading symbol from " LIBNAME "\n\n%s", load_library_errmsg ); \ + CLOSE_LIBRARY; \ + exit(1); \ +} + +#define DL(x) dl_##x +#define CLOSE_LIBRARY dlclose(libhandle); + + +/*** libfuse stuff ***/ + +#define FUSE_ROOT_ID 1 +#define FUSE_ARGS_INIT(argc, argv) { argc, argv, 0 } +#define FUSE_OPT_KEY(templ, key) { templ, -1U, key } +#define FUSE_OPT_KEY_OPT -1 +#define FUSE_OPT_KEY_NONOPT -2 +#define FUSE_OPT_END { NULL, 0, 0 } + +enum fuse_buf_flags { + FUSE_BUF_IS_FD = (1 << 1), + FUSE_BUF_FD_SEEK = (1 << 2), + FUSE_BUF_FD_RETRY = (1 << 3), +}; + +typedef unsigned long fuse_ino_t; +typedef struct fuse_req *fuse_req_t; + +struct fuse_chan; +struct fuse_pollhandle; + +struct fuse_args { + int argc; + char **argv; + int allocated; +}; + +typedef int (*fuse_fill_dir_t) (void *buf, const char *name, const struct stat *stbuf, off_t off); +typedef int (*fuse_opt_proc_t)(void *data, const char *arg, int key, struct fuse_args *outargs); +typedef struct fuse_dirhandle *fuse_dirh_t; +typedef int (*fuse_dirfil_t) (fuse_dirh_t h, const char *name, int type, ino_t ino); + +struct fuse_file_info { + int flags; + unsigned long fh_old; + int writepage; + unsigned int direct_io : 1; + unsigned int keep_cache : 1; + unsigned int flush : 1; + unsigned int nonseekable : 1; + unsigned int flock_release : 1; + unsigned int padding : 27; + uint64_t fh; + uint64_t lock_owner; +}; + +struct fuse_entry_param { + fuse_ino_t ino; + unsigned long generation; + struct stat attr; + double attr_timeout; + double entry_timeout; +}; + +struct fuse_opt { + const char *templ; + unsigned long offset; + int value; +}; + +struct fuse_forget_data { + uint64_t ino; + uint64_t nlookup; +}; + +struct fuse_conn_info { + unsigned proto_major; + unsigned proto_minor; + unsigned async_read; + unsigned max_write; + unsigned max_readahead; + unsigned capable; + unsigned want; + unsigned max_background; + unsigned congestion_threshold; + unsigned reserved[23]; +}; + +struct fuse_buf { + size_t size; + enum fuse_buf_flags flags; + void *mem; + int fd; + off_t pos; +}; + +struct fuse_bufvec { + size_t count; + size_t idx; + size_t off; + struct fuse_buf buf[1]; +}; + +struct fuse_context { + struct fuse *fuse; + uid_t uid; + gid_t gid; + pid_t pid; + void *private_data; + mode_t umask; +}; + +struct fuse_operations { + int (*getattr) (const char *, struct stat *); + int (*readlink) (const char *, char *, size_t); + int (*getdir) (const char *, fuse_dirh_t, fuse_dirfil_t); + int (*mknod) (const char *, mode_t, dev_t); + int (*mkdir) (const char *, mode_t); + int (*unlink) (const char *); + int (*rmdir) (const char *); + int (*symlink) (const char *, const char *); + int (*rename) (const char *, const char *); + int (*link) (const char *, const char *); + int (*chmod) (const char *, mode_t); + int (*chown) (const char *, uid_t, gid_t); + int (*truncate) (const char *, off_t); + int (*utime) (const char *, struct utimbuf *); + int (*open) (const char *, struct fuse_file_info *); + int (*read) (const char *, char *, size_t, off_t, struct fuse_file_info *); + int (*write) (const char *, const char *, size_t, off_t, struct fuse_file_info *); + int (*statfs) (const char *, struct statvfs *); + int (*flush) (const char *, struct fuse_file_info *); + int (*release) (const char *, struct fuse_file_info *); + int (*fsync) (const char *, int, struct fuse_file_info *); + int (*setxattr) (const char *, const char *, const char *, size_t, int); + int (*getxattr) (const char *, const char *, char *, size_t); + int (*listxattr) (const char *, char *, size_t); + int (*removexattr) (const char *, const char *); + int (*opendir) (const char *, struct fuse_file_info *); + int (*readdir) (const char *, void *, fuse_fill_dir_t, off_t, struct fuse_file_info *); + int (*releasedir) (const char *, struct fuse_file_info *); + int (*fsyncdir) (const char *, int, struct fuse_file_info *); + void *(*init) (struct fuse_conn_info *conn); + void (*destroy) (void *); + int (*access) (const char *, int); + int (*create) (const char *, mode_t, struct fuse_file_info *); + int (*ftruncate) (const char *, off_t, struct fuse_file_info *); + int (*fgetattr) (const char *, struct stat *, struct fuse_file_info *); + int (*lock) (const char *, struct fuse_file_info *, int cmd, struct flock *); + int (*utimens) (const char *, const struct timespec tv[2]); + int (*bmap) (const char *, size_t blocksize, uint64_t *idx); + unsigned int flag_nullpath_ok:1; + unsigned int flag_nopath:1; + unsigned int flag_utime_omit_ok:1; + unsigned int flag_reserved:29; + int (*ioctl) (const char *, int cmd, void *arg, struct fuse_file_info *, unsigned int flags, void *data); + int (*poll) (const char *, struct fuse_file_info *, struct fuse_pollhandle *ph, unsigned *reventsp); + int (*write_buf) (const char *, struct fuse_bufvec *buf, off_t off, struct fuse_file_info *); + int (*read_buf) (const char *, struct fuse_bufvec **bufp, size_t size, off_t off, struct fuse_file_info *); + int (*flock) (const char *, struct fuse_file_info *, int op); + int (*fallocate) (const char *, int, off_t, off_t, struct fuse_file_info *); +}; + +struct fuse_lowlevel_ops { + void (*init) (void *userdata, struct fuse_conn_info *conn); + void (*destroy) (void *userdata); + void (*lookup) (fuse_req_t req, fuse_ino_t parent, const char *name); + void (*forget) (fuse_req_t req, fuse_ino_t ino, unsigned long nlookup); + void (*getattr) (fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi); + void (*setattr) (fuse_req_t req, fuse_ino_t ino, struct stat *attr, int to_set, struct fuse_file_info *fi); + void (*readlink) (fuse_req_t req, fuse_ino_t ino); + void (*mknod) (fuse_req_t req, fuse_ino_t parent, const char *name, mode_t mode, dev_t rdev); + void (*mkdir) (fuse_req_t req, fuse_ino_t parent, const char *name, mode_t mode); + void (*unlink) (fuse_req_t req, fuse_ino_t parent, const char *name); + void (*rmdir) (fuse_req_t req, fuse_ino_t parent, const char *name); + void (*symlink) (fuse_req_t req, const char *link, fuse_ino_t parent, const char *name); + void (*rename) (fuse_req_t req, fuse_ino_t parent, const char *name, fuse_ino_t newparent, const char *newname); + void (*link) (fuse_req_t req, fuse_ino_t ino, fuse_ino_t newparent, const char *newname); + void (*open) (fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi); + void (*read) (fuse_req_t req, fuse_ino_t ino, size_t size, off_t off, struct fuse_file_info *fi); + void (*write) (fuse_req_t req, fuse_ino_t ino, const char *buf, size_t size, off_t off, struct fuse_file_info *fi); + void (*flush) (fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi); + void (*release) (fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi); + void (*fsync) (fuse_req_t req, fuse_ino_t ino, int datasync, struct fuse_file_info *fi); + void (*opendir) (fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi); + void (*readdir) (fuse_req_t req, fuse_ino_t ino, size_t size, off_t off, struct fuse_file_info *fi); + void (*releasedir) (fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi); + void (*fsyncdir) (fuse_req_t req, fuse_ino_t ino, int datasync, struct fuse_file_info *fi); + void (*statfs) (fuse_req_t req, fuse_ino_t ino); + void (*setxattr) (fuse_req_t req, fuse_ino_t ino, const char *name, const char *value, size_t size, int flags); + void (*getxattr) (fuse_req_t req, fuse_ino_t ino, const char *name, size_t size); + void (*listxattr) (fuse_req_t req, fuse_ino_t ino, size_t size); + void (*removexattr) (fuse_req_t req, fuse_ino_t ino, const char *name); + void (*access) (fuse_req_t req, fuse_ino_t ino, int mask); + void (*create) (fuse_req_t req, fuse_ino_t parent, const char *name, mode_t mode, struct fuse_file_info *fi); + void (*getlk) (fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi, struct flock *lock); + void (*setlk) (fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi, struct flock *lock, int sleep); + void (*bmap) (fuse_req_t req, fuse_ino_t ino, size_t blocksize, uint64_t idx); + void (*ioctl) (fuse_req_t req, fuse_ino_t ino, int cmd, void *arg, struct fuse_file_info *fi, unsigned flags, const void *in_buf, size_t in_bufsz, size_t out_bufsz); + void (*poll) (fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi, struct fuse_pollhandle *ph); + void (*write_buf) (fuse_req_t req, fuse_ino_t ino, struct fuse_bufvec *bufv, off_t off, struct fuse_file_info *fi); + void (*retrieve_reply) (fuse_req_t req, void *cookie, fuse_ino_t ino, off_t offset, struct fuse_bufvec *bufv); + void (*forget_multi) (fuse_req_t req, size_t count, struct fuse_forget_data *forgets); + void (*flock) (fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi, int op); + void (*fallocate) (fuse_req_t req, fuse_ino_t ino, int mode, off_t offset, off_t length, struct fuse_file_info *fi); +}; + +#else /* !ENABLE_DLOPEN */ + +#define LOAD_LIBRARY +#define LOAD_SYMBOL(x) +#define DL(x) +#define CLOSE_LIBRARY + +#endif /* !ENABLE_DLOPEN */ + +#endif /* SQFS_DLOPEN_H */ + diff --git a/src/patches/squashfuse_dlopen.patch b/src/patches/squashfuse_dlopen.patch new file mode 100644 index 0000000..59e1e6e --- /dev/null +++ b/src/patches/squashfuse_dlopen.patch @@ -0,0 +1,640 @@ +--- a/Makefile.am ++++ b/Makefile.am +@@ -1,6 +1,7 @@ + COMPRESSION_LIBS = $(ZLIB_LIBS) $(XZ_LIBS) $(LZO_LIBS) $(LZ4_LIBS) + + ACLOCAL_AMFLAGS = -I m4 --install ++AM_CFLAGS = -fno-strict-aliasing -DENABLE_DLOPEN + + # Suppress AppleDouble + if MAKE_EXPORT +@@ -19,13 +20,13 @@ + # Main library: libsquashfuse + libsquashfuse_la_SOURCES = swap.c cache.c table.c dir.c file.c fs.c \ + decompress.c xattr.c hash.c stack.c traverse.c util.c \ +- nonstd-pread.c nonstd-stat.c \ ++ nonstd-pread.c nonstd-stat.c squashfuse_dlopen.c \ + squashfs_fs.h common.h nonstd-internal.h nonstd.h swap.h cache.h table.h \ + dir.h file.h decompress.h xattr.h squashfuse.h hash.h stack.h traverse.h \ +- util.h fs.h ++ util.h fs.h squashfuse_dlopen.h + libsquashfuse_la_CPPFLAGS = $(ZLIB_CPPFLAGS) $(XZ_CPPFLAGS) $(LZO_CPPFLAGS) \ + $(LZ4_CPPFLAGS) +-libsquashfuse_la_LIBADD = ++libsquashfuse_la_LIBADD = -ldl + + # Helper for FUSE clients: libfuseprivate + libfuseprivate_la_SOURCES = fuseprivate.c nonstd-makedev.c nonstd-enoattr.c \ +--- a/fuseprivate.c ++++ b/fuseprivate.c +@@ -94,15 +94,17 @@ + } + + void sqfs_usage(char *progname, bool fuse_usage) { ++ LOAD_SYMBOL(int,fuse_opt_add_arg,(struct fuse_args *args, const char *arg)); ++ LOAD_SYMBOL(int,fuse_parse_cmdline,(struct fuse_args *args, char **mountpoint, int *multithreaded, int *foreground)); + fprintf(stderr, "%s (c) 2012 Dave Vasilevsky\n\n", PACKAGE_STRING); + fprintf(stderr, "Usage: %s [options] ARCHIVE MOUNTPOINT\n", + progname ? progname : PACKAGE_NAME); + if (fuse_usage) { + struct fuse_args args = FUSE_ARGS_INIT(0, NULL); +- fuse_opt_add_arg(&args, ""); /* progname */ +- fuse_opt_add_arg(&args, "-ho"); ++ DL(fuse_opt_add_arg)(&args, ""); /* progname */ ++ DL(fuse_opt_add_arg)(&args, "-ho"); + fprintf(stderr, "\n"); +- fuse_parse_cmdline(&args, NULL, NULL, NULL); ++ DL(fuse_parse_cmdline)(&args, NULL, NULL, NULL); + } + exit(-2); + } +--- a/fuseprivate.h ++++ b/fuseprivate.h +@@ -27,7 +27,10 @@ + + #include "squashfuse.h" + +-#include ++#include "squashfuse_dlopen.h" ++#ifndef ENABLE_DLOPEN ++# include ++#endif + + #include + +--- a/hl.c ++++ b/hl.c +@@ -33,6 +33,7 @@ + #include + #include + ++int have_libloaded = 0; + + typedef struct sqfs_hl sqfs_hl; + struct sqfs_hl { +@@ -42,9 +43,10 @@ + + static sqfs_err sqfs_hl_lookup(sqfs **fs, sqfs_inode *inode, + const char *path) { ++ LOAD_SYMBOL(struct fuse_context *,fuse_get_context,(void)); + bool found; + +- sqfs_hl *hl = fuse_get_context()->private_data; ++ sqfs_hl *hl = DL(fuse_get_context)()->private_data; + *fs = &hl->fs; + if (inode) + *inode = hl->root; /* copy */ +@@ -67,7 +69,8 @@ + } + + static void *sqfs_hl_op_init(struct fuse_conn_info *conn) { +- return fuse_get_context()->private_data; ++ LOAD_SYMBOL(struct fuse_context *,fuse_get_context,(void)); ++ return DL(fuse_get_context)()->private_data; + } + + static int sqfs_hl_op_getattr(const char *path, struct stat *st) { +@@ -264,7 +267,16 @@ + return NULL; + } + ++#ifdef ENABLE_DLOPEN ++#define fuse_main(argc, argv, op, user_data) \ ++ DL(fuse_main_real)(argc, argv, op, sizeof(*(op)), user_data) ++#endif ++ + int main(int argc, char *argv[]) { ++ LOAD_SYMBOL(int,fuse_opt_parse,(struct fuse_args *args, void *data, const struct fuse_opt opts[], fuse_opt_proc_t proc)); ++ LOAD_SYMBOL(int,fuse_opt_add_arg,(struct fuse_args *args, const char *arg)); ++ LOAD_SYMBOL(int,fuse_main_real,(int argc, char *argv[], const struct fuse_operations *op, size_t op_size, void *user_data)); /* fuse_main */ ++ LOAD_SYMBOL(void,fuse_opt_free_args,(struct fuse_args *args)); + struct fuse_args args; + sqfs_opts opts; + sqfs_hl *hl; +@@ -299,7 +311,7 @@ + opts.image = NULL; + opts.mountpoint = 0; + opts.offset = 0; +- if (fuse_opt_parse(&args, &opts, fuse_opts, sqfs_opt_proc) == -1) ++ if (DL(fuse_opt_parse)(&args, &opts, fuse_opts, sqfs_opt_proc) == -1) + sqfs_usage(argv[0], true); + if (!opts.image) + sqfs_usage(argv[0], true); +@@ -308,8 +320,9 @@ + if (!hl) + return -1; + +- fuse_opt_add_arg(&args, "-s"); /* single threaded */ ++ DL(fuse_opt_add_arg)(&args, "-s"); /* single threaded */ + ret = fuse_main(args.argc, args.argv, &sqfs_hl_ops, hl); +- fuse_opt_free_args(&args); ++ DL(fuse_opt_free_args)(&args); ++ CLOSE_LIBRARY; + return ret; + } +--- a/ll.h ++++ b/ll.h +@@ -27,7 +27,10 @@ + + #include "squashfuse.h" + +-#include ++#include "squashfuse_dlopen.h" ++#ifndef ENABLE_DLOPEN ++# include ++#endif + + typedef struct sqfs_ll sqfs_ll; + struct sqfs_ll { +--- a/ll_inode.c ++++ b/ll_inode.c +@@ -348,12 +348,14 @@ + + + sqfs_err sqfs_ll_iget(fuse_req_t req, sqfs_ll_i *lli, fuse_ino_t i) { ++ LOAD_SYMBOL(void *,fuse_req_userdata,(fuse_req_t req)); ++ LOAD_SYMBOL(int,fuse_reply_err,(fuse_req_t req, int err)); + sqfs_err err = SQFS_OK; +- lli->ll = fuse_req_userdata(req); ++ lli->ll = DL(fuse_req_userdata)(req); + if (i != SQFS_FUSE_INODE_NONE) { + err = sqfs_ll_inode(lli->ll, &lli->inode, i); + if (err) +- fuse_reply_err(req, ENOENT); ++ DL(fuse_reply_err)(req, ENOENT); + } + return err; + } +--- a/nonstd-daemon.c ++++ b/nonstd-daemon.c +@@ -28,11 +28,16 @@ + #include "nonstd-internal.h" + + #include +-#include ++ ++#include "squashfuse_dlopen.h" ++#ifndef ENABLE_DLOPEN ++# include ++#endif + + int sqfs_ll_daemonize(int fg) { + #if HAVE_DECL_FUSE_DAEMONIZE +- return fuse_daemonize(fg); ++ LOAD_SYMBOL(int,fuse_daemonize,(int foreground)); ++ return DL(fuse_daemonize)(fg); + #else + return daemon(0,0); + #endif +--- a/ll.c ++++ b/ll.c +@@ -38,37 +38,41 @@ + + static void sqfs_ll_op_getattr(fuse_req_t req, fuse_ino_t ino, + struct fuse_file_info *fi) { ++ LOAD_SYMBOL(int,fuse_reply_err,(fuse_req_t req, int err)); ++ LOAD_SYMBOL(int,fuse_reply_attr,(fuse_req_t req, const struct stat *attr, double attr_timeout)); + sqfs_ll_i lli; + struct stat st; + if (sqfs_ll_iget(req, &lli, ino)) + return; + + if (sqfs_stat(&lli.ll->fs, &lli.inode, &st)) { +- fuse_reply_err(req, ENOENT); ++ DL(fuse_reply_err)(req, ENOENT); + } else { + st.st_ino = ino; +- fuse_reply_attr(req, &st, SQFS_TIMEOUT); ++ DL(fuse_reply_attr)(req, &st, SQFS_TIMEOUT); + } + } + + static void sqfs_ll_op_opendir(fuse_req_t req, fuse_ino_t ino, + struct fuse_file_info *fi) { ++ LOAD_SYMBOL(int,fuse_reply_err,(fuse_req_t req, int err)); ++ LOAD_SYMBOL(int,fuse_reply_open,(fuse_req_t req, const struct fuse_file_info *fi)); + sqfs_ll_i *lli; + + fi->fh = (intptr_t)NULL; + + lli = malloc(sizeof(*lli)); + if (!lli) { +- fuse_reply_err(req, ENOMEM); ++ DL(fuse_reply_err)(req, ENOMEM); + return; + } + + if (sqfs_ll_iget(req, lli, ino) == SQFS_OK) { + if (!S_ISDIR(lli->inode.base.mode)) { +- fuse_reply_err(req, ENOTDIR); ++ DL(fuse_reply_err)(req, ENOTDIR); + } else { + fi->fh = (intptr_t)lli; +- fuse_reply_open(req, fi); ++ DL(fuse_reply_open)(req, fi); + return; + } + } +@@ -77,28 +81,35 @@ + + static void sqfs_ll_op_create(fuse_req_t req, fuse_ino_t parent, const char *name, + mode_t mode, struct fuse_file_info *fi) { +- fuse_reply_err(req, EROFS); ++ LOAD_SYMBOL(int,fuse_reply_err,(fuse_req_t req, int err)); ++ DL(fuse_reply_err)(req, EROFS); + } + + static void sqfs_ll_op_releasedir(fuse_req_t req, fuse_ino_t ino, + struct fuse_file_info *fi) { + free((sqfs_ll_i*)(intptr_t)fi->fh); +- fuse_reply_err(req, 0); /* yes, this is necessary */ ++ LOAD_SYMBOL(int,fuse_reply_err,(fuse_req_t req, int err)); ++ DL(fuse_reply_err)(req, 0); /* yes, this is necessary */ + } + + static size_t sqfs_ll_add_direntry(fuse_req_t req, char *buf, size_t bufsize, + const char *name, const struct stat *st, off_t off) { + #if HAVE_DECL_FUSE_ADD_DIRENTRY +- return fuse_add_direntry(req, buf, bufsize, name, st, off); ++ LOAD_SYMBOL(size_t,fuse_add_direntry,(fuse_req_t req, char *buf, size_t bufsize, const char *name, const struct stat *stbuf, off_t off)); ++ return DL(fuse_add_direntry)(req, buf, bufsize, name, st, off); + #else +- size_t esize = fuse_dirent_size(strlen(name)); ++ LOAD_SYMBOL(size_t,fuse_dirent_size(size_t namelen)); ++ LOAD_SYMBOL(char *,fuse_add_dirent,(char *buf, const char *name, const struct stat *stbuf, off_t off)); ++ size_t esize = DL(fuse_dirent_size)(strlen(name)); + if (bufsize >= esize) +- fuse_add_dirent(buf, name, st, off); ++ DL(fuse_add_dirent)(buf, name, st, off); + return esize; + #endif + } + static void sqfs_ll_op_readdir(fuse_req_t req, fuse_ino_t ino, size_t size, + off_t off, struct fuse_file_info *fi) { ++ LOAD_SYMBOL(int,fuse_reply_err,(fuse_req_t req, int err)); ++ LOAD_SYMBOL(int,fuse_reply_buf,(fuse_req_t req, const char *buf, size_t size)); + sqfs_err sqerr; + sqfs_dir dir; + sqfs_name namebuf; +@@ -135,14 +146,16 @@ + } + + if (err) +- fuse_reply_err(req, err); ++ DL(fuse_reply_err)(req, err); + else +- fuse_reply_buf(req, buf, bufpos - buf); ++ DL(fuse_reply_buf)(req, buf, bufpos - buf); + free(buf); + } + + static void sqfs_ll_op_lookup(fuse_req_t req, fuse_ino_t parent, + const char *name) { ++ LOAD_SYMBOL(int,fuse_reply_err,(fuse_req_t req, int err)); ++ LOAD_SYMBOL(int,fuse_reply_entry,(fuse_req_t req, const struct fuse_entry_param *e)); + sqfs_ll_i lli; + sqfs_err sqerr; + sqfs_name namebuf; +@@ -154,7 +167,7 @@ + return; + + if (!S_ISDIR(lli.inode.base.mode)) { +- fuse_reply_err(req, ENOTDIR); ++ DL(fuse_reply_err)(req, ENOTDIR); + return; + } + +@@ -162,55 +175,58 @@ + sqerr = sqfs_dir_lookup(&lli.ll->fs, &lli.inode, name, strlen(name), &entry, + &found); + if (sqerr) { +- fuse_reply_err(req, EIO); ++ DL(fuse_reply_err)(req, EIO); + return; + } + if (!found) { +- fuse_reply_err(req, ENOENT); ++ DL(fuse_reply_err)(req, ENOENT); + return; + } + + if (sqfs_inode_get(&lli.ll->fs, &inode, sqfs_dentry_inode(&entry))) { +- fuse_reply_err(req, ENOENT); ++ DL(fuse_reply_err)(req, ENOENT); + } else { + struct fuse_entry_param fentry; + memset(&fentry, 0, sizeof(fentry)); + if (sqfs_stat(&lli.ll->fs, &inode, &fentry.attr)) { +- fuse_reply_err(req, EIO); ++ DL(fuse_reply_err)(req, EIO); + } else { + fentry.attr_timeout = fentry.entry_timeout = SQFS_TIMEOUT; + fentry.ino = lli.ll->ino_register(lli.ll, &entry); + fentry.attr.st_ino = fentry.ino; +- fuse_reply_entry(req, &fentry); ++ DL(fuse_reply_entry)(req, &fentry); + } + } + } + + static void sqfs_ll_op_open(fuse_req_t req, fuse_ino_t ino, + struct fuse_file_info *fi) { ++ LOAD_SYMBOL(int,fuse_reply_err,(fuse_req_t req, int err)); ++ LOAD_SYMBOL(int,fuse_reply_open,(fuse_req_t req, const struct fuse_file_info *fi)); ++ LOAD_SYMBOL(void *,fuse_req_userdata,(fuse_req_t req)); + sqfs_inode *inode; + sqfs_ll *ll; + + if (fi->flags & (O_WRONLY | O_RDWR)) { +- fuse_reply_err(req, EROFS); ++ DL(fuse_reply_err)(req, EROFS); + return; + } + + inode = malloc(sizeof(sqfs_inode)); + if (!inode) { +- fuse_reply_err(req, ENOMEM); ++ DL(fuse_reply_err)(req, ENOMEM); + return; + } + +- ll = fuse_req_userdata(req); ++ ll = DL(fuse_req_userdata)(req); + if (sqfs_ll_inode(ll, inode, ino)) { +- fuse_reply_err(req, ENOENT); ++ DL(fuse_reply_err)(req, ENOENT); + } else if (!S_ISREG(inode->base.mode)) { +- fuse_reply_err(req, EISDIR); ++ DL(fuse_reply_err)(req, EISDIR); + } else { + fi->fh = (intptr_t)inode; + fi->keep_cache = 1; +- fuse_reply_open(req, fi); ++ DL(fuse_reply_open)(req, fi); + return; + } + free(inode); +@@ -218,37 +234,43 @@ + + static void sqfs_ll_op_release(fuse_req_t req, fuse_ino_t ino, + struct fuse_file_info *fi) { ++ LOAD_SYMBOL(int,fuse_reply_err,(fuse_req_t req, int err)); + free((sqfs_inode*)(intptr_t)fi->fh); + fi->fh = 0; +- fuse_reply_err(req, 0); ++ DL(fuse_reply_err)(req, 0); + } + + static void sqfs_ll_op_read(fuse_req_t req, fuse_ino_t ino, + size_t size, off_t off, struct fuse_file_info *fi) { +- sqfs_ll *ll = fuse_req_userdata(req); ++ LOAD_SYMBOL(void *,fuse_req_userdata,(fuse_req_t req)); ++ LOAD_SYMBOL(int,fuse_reply_err,(fuse_req_t req, int err)); ++ LOAD_SYMBOL(int,fuse_reply_buf,(fuse_req_t req, const char *buf, size_t size)); ++ sqfs_ll *ll = DL(fuse_req_userdata)(req); + sqfs_inode *inode = (sqfs_inode*)(intptr_t)fi->fh; + sqfs_err err = SQFS_OK; + + off_t osize; + char *buf = malloc(size); + if (!buf) { +- fuse_reply_err(req, ENOMEM); ++ DL(fuse_reply_err)(req, ENOMEM); + return; + } + + osize = size; + err = sqfs_read_range(&ll->fs, inode, off, &osize, buf); + if (err) { +- fuse_reply_err(req, EIO); ++ DL(fuse_reply_err)(req, EIO); + } else if (osize == 0) { /* EOF */ +- fuse_reply_buf(req, NULL, 0); ++ DL(fuse_reply_buf)(req, NULL, 0); + } else { +- fuse_reply_buf(req, buf, osize); ++ DL(fuse_reply_buf)(req, buf, osize); + } + free(buf); + } + + static void sqfs_ll_op_readlink(fuse_req_t req, fuse_ino_t ino) { ++ LOAD_SYMBOL(int,fuse_reply_err,(fuse_req_t req, int err)); ++ LOAD_SYMBOL(int,fuse_reply_readlink,(fuse_req_t req, const char *link)); + char *dst; + size_t size; + sqfs_ll_i lli; +@@ -256,21 +278,24 @@ + return; + + if (!S_ISLNK(lli.inode.base.mode)) { +- fuse_reply_err(req, EINVAL); ++ DL(fuse_reply_err)(req, EINVAL); + } else if (sqfs_readlink(&lli.ll->fs, &lli.inode, NULL, &size)) { +- fuse_reply_err(req, EIO); ++ DL(fuse_reply_err)(req, EIO); + } else if (!(dst = malloc(size + 1))) { +- fuse_reply_err(req, ENOMEM); ++ DL(fuse_reply_err)(req, ENOMEM); + } else if (sqfs_readlink(&lli.ll->fs, &lli.inode, dst, &size)) { +- fuse_reply_err(req, EIO); ++ DL(fuse_reply_err)(req, EIO); + free(dst); + } else { +- fuse_reply_readlink(req, dst); ++ DL(fuse_reply_readlink)(req, dst); + free(dst); + } + } + + static void sqfs_ll_op_listxattr(fuse_req_t req, fuse_ino_t ino, size_t size) { ++ LOAD_SYMBOL(int,fuse_reply_err,(fuse_req_t req, int err)); ++ LOAD_SYMBOL(int,fuse_reply_xattr,(fuse_req_t req, size_t count)); ++ LOAD_SYMBOL(int,fuse_reply_buf,(fuse_req_t req, const char *buf, size_t size)); + sqfs_ll_i lli; + char *buf; + int ferr; +@@ -280,17 +305,17 @@ + + buf = NULL; + if (size && !(buf = malloc(size))) { +- fuse_reply_err(req, ENOMEM); ++ DL(fuse_reply_err)(req, ENOMEM); + return; + } + + ferr = sqfs_listxattr(&lli.ll->fs, &lli.inode, buf, &size); + if (ferr) { +- fuse_reply_err(req, ferr); ++ DL(fuse_reply_err)(req, ferr); + } else if (buf) { +- fuse_reply_buf(req, buf, size); ++ DL(fuse_reply_buf)(req, buf, size); + } else { +- fuse_reply_xattr(req, size); ++ DL(fuse_reply_xattr)(req, size); + } + free(buf); + } +@@ -301,13 +326,16 @@ + , uint32_t position + #endif + ) { ++ LOAD_SYMBOL(int,fuse_reply_err,(fuse_req_t req, int err)); ++ LOAD_SYMBOL(int,fuse_reply_xattr,(fuse_req_t req, size_t count)); ++ LOAD_SYMBOL(int,fuse_reply_buf,(fuse_req_t req, const char *buf, size_t size)); + sqfs_ll_i lli; + char *buf = NULL; + size_t real = size; + + #ifdef FUSE_XATTR_POSITION + if (position != 0) { /* We don't support resource forks */ +- fuse_reply_err(req, EINVAL); ++ DL(fuse_reply_err)(req, EINVAL); + return; + } + #endif +@@ -316,26 +344,27 @@ + return; + + if (!(buf = malloc(size))) +- fuse_reply_err(req, ENOMEM); ++ DL(fuse_reply_err)(req, ENOMEM); + else if (sqfs_xattr_lookup(&lli.ll->fs, &lli.inode, name, buf, &real)) +- fuse_reply_err(req, EIO); ++ DL(fuse_reply_err)(req, EIO); + else if (real == 0) +- fuse_reply_err(req, sqfs_enoattr()); ++ DL(fuse_reply_err)(req, sqfs_enoattr()); + else if (size == 0) +- fuse_reply_xattr(req, real); ++ DL(fuse_reply_xattr)(req, real); + else if (size < real) +- fuse_reply_err(req, ERANGE); ++ DL(fuse_reply_err)(req, ERANGE); + else +- fuse_reply_buf(req, buf, real); ++ DL(fuse_reply_buf)(req, buf, real); + free(buf); + } + + static void sqfs_ll_op_forget(fuse_req_t req, fuse_ino_t ino, + unsigned long nlookup) { ++ LOAD_SYMBOL(void,fuse_reply_none,(fuse_req_t req)); + sqfs_ll_i lli; + sqfs_ll_iget(req, &lli, SQFS_FUSE_INODE_NONE); + lli.ll->ino_forget(lli.ll, ino, nlookup); +- fuse_reply_none(req); ++ DL(fuse_reply_none)(req); + } + + +@@ -348,23 +377,27 @@ + + static sqfs_err sqfs_ll_mount(sqfs_ll_chan *ch, const char *mountpoint, + struct fuse_args *args) { ++ LOAD_SYMBOL(struct fuse_chan *,fuse_mount,(const char *mountpoint, struct fuse_args *args)); + #ifdef HAVE_NEW_FUSE_UNMOUNT +- ch->ch = fuse_mount(mountpoint, args); ++ ch->ch = DL(fuse_mount)(mountpoint, args); + #else +- ch->fd = fuse_mount(mountpoint, args); ++ LOAD_SYMBOL(struct fuse_chan *,fuse_kern_chan_new,(int fd)); ++ ch->fd = DL(fuse_mount)(mountpoint, args); + if (ch->fd == -1) + return SQFS_ERR; +- ch->ch = fuse_kern_chan_new(ch->fd); ++ ch->ch = DL(fuse_kern_chan_new)(ch->fd); + #endif + return ch->ch ? SQFS_OK : SQFS_ERR; + } + + static void sqfs_ll_unmount(sqfs_ll_chan *ch, const char *mountpoint) { + #ifdef HAVE_NEW_FUSE_UNMOUNT +- fuse_unmount(mountpoint, ch->ch); ++ LOAD_SYMBOL(void,fuse_unmount,(const char *mountpoint, struct fuse_chan *ch)); ++ DL(fuse_unmount)(mountpoint, ch->ch); + #else ++ LOAD_SYMBOL(void,fuse_unmount,(const char *mountpoint)); + close(ch->fd); +- fuse_unmount(mountpoint); ++ DL(fuse_unmount)(mountpoint); + #endif + } + +@@ -391,6 +424,19 @@ + } + + int fusefs_main(int argc, char *argv[], void (*mounted) (void)) { ++ LOAD_SYMBOL(int,fuse_opt_parse,(struct fuse_args *args, void *data, const struct fuse_opt opts[], fuse_opt_proc_t proc)); ++ LOAD_SYMBOL(int,fuse_parse_cmdline,(struct fuse_args *args, char **mountpoint, int *multithreaded, int *foreground)); ++ LOAD_SYMBOL(struct fuse_session *,fuse_lowlevel_new,(struct fuse_args *args, const struct fuse_lowlevel_ops *op, size_t op_size, void *userdata)); ++ LOAD_SYMBOL(int,fuse_set_signal_handlers,(struct fuse_session *se)); ++ LOAD_SYMBOL(void,fuse_session_add_chan,(struct fuse_session *se, struct fuse_chan *ch)); ++ LOAD_SYMBOL(int,fuse_session_loop,(struct fuse_session *se)); ++ LOAD_SYMBOL(void,fuse_remove_signal_handlers,(struct fuse_session *se)); ++#if HAVE_DECL_FUSE_SESSION_REMOVE_CHAN ++ LOAD_SYMBOL(void,fuse_session_remove_chan,(struct fuse_chan *ch)); ++#endif ++ LOAD_SYMBOL(void,fuse_session_destroy,(struct fuse_session *se)); ++ LOAD_SYMBOL(void,fuse_opt_free_args,(struct fuse_args *args)); ++ + struct fuse_args args; + sqfs_opts opts; + +@@ -429,10 +475,10 @@ + opts.image = NULL; + opts.mountpoint = 0; + opts.offset = 0; +- if (fuse_opt_parse(&args, &opts, fuse_opts, sqfs_opt_proc) == -1) ++ if (DL(fuse_opt_parse)(&args, &opts, fuse_opts, sqfs_opt_proc) == -1) + sqfs_usage(argv[0], true); + +- if (fuse_parse_cmdline(&args, &mountpoint, &mt, &fg) == -1) ++ if (DL(fuse_parse_cmdline)(&args, &mountpoint, &mt, &fg) == -1) + sqfs_usage(argv[0], true); + if (mountpoint == NULL) + sqfs_usage(argv[0], true); +@@ -445,33 +491,34 @@ + sqfs_ll_chan ch; + err = -1; + if (sqfs_ll_mount(&ch, mountpoint, &args) == SQFS_OK) { +- struct fuse_session *se = fuse_lowlevel_new(&args, ++ struct fuse_session *se = DL(fuse_lowlevel_new)(&args, + &sqfs_ll_ops, sizeof(sqfs_ll_ops), ll); + if (se != NULL) { + if (sqfs_ll_daemonize(fg) != -1) { +- if (fuse_set_signal_handlers(se) != -1) { +- fuse_session_add_chan(se, ch.ch); ++ if (DL(fuse_set_signal_handlers)(se) != -1) { ++ DL(fuse_session_add_chan)(se, ch.ch); + if (mounted) + mounted (); + /* FIXME: multithreading */ +- err = fuse_session_loop(se); +- fuse_remove_signal_handlers(se); ++ err = DL(fuse_session_loop)(se); ++ DL(fuse_remove_signal_handlers)(se); + #if HAVE_DECL_FUSE_SESSION_REMOVE_CHAN +- fuse_session_remove_chan(ch.ch); ++ DL(fuse_session_remove_chan)(ch.ch); + #endif + } + } +- fuse_session_destroy(se); ++ DL(fuse_session_destroy)(se); + } + sqfs_ll_destroy(ll); + sqfs_ll_unmount(&ch, mountpoint); + } + } +- fuse_opt_free_args(&args); ++ DL(fuse_opt_free_args)(&args); + if (mounted) + rmdir (mountpoint); + free(ll); + free(mountpoint); ++ CLOSE_LIBRARY; + + return -err; + } diff --git a/src/xdg-basedir/CMakeLists.txt b/src/xdg-basedir/CMakeLists.txt new file mode 100644 index 0000000..064260d --- /dev/null +++ b/src/xdg-basedir/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 3.5) + +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +# force static linking +add_library(xdg-basedir STATIC xdg-basedir.h xdg-basedir.c) +target_include_directories(xdg-basedir PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/src/xdg-basedir/xdg-basedir.c b/src/xdg-basedir/xdg-basedir.c new file mode 100644 index 0000000..c154c76 --- /dev/null +++ b/src/xdg-basedir/xdg-basedir.c @@ -0,0 +1,67 @@ +#include "xdg-basedir.h" +#include +#include + +char* user_home() { + return strdup(getenv("HOME")); +} + +char* xdg_config_home() { + char* config_home = getenv("XDG_CONFIG_HOME"); + + if (config_home == NULL) { + char* home = user_home(); + static const char const* suffix = "/.config"; + + config_home = calloc(strlen(home) + strlen(suffix) + 1, sizeof(char)); + + strcpy(config_home, home); + strcat(config_home, suffix); + + free(home); + + return config_home; + } else { + return strdup(config_home); + } +} + +char* xdg_data_home() { + char* data_home = getenv("XDG_DATA_HOME"); + + if (data_home == NULL) { + char* home = user_home(); + static const char const* suffix = "/.local/share"; + + data_home = calloc(strlen(home) + strlen(suffix) + 1, sizeof(char)); + + strcpy(data_home, home); + strcat(data_home, suffix); + + free(home); + + return data_home; + } else { + return strdup(data_home); + } +} + +char* xdg_cache_home() { + char* cache_home = getenv("XDG_CACHE_HOME"); + + if (cache_home == NULL) { + char* home = user_home(); + static const char const* suffix = "/.cache"; + + cache_home = calloc(strlen(home) + strlen(suffix) + 1, sizeof(char)); + + strcpy(cache_home, home); + strcat(cache_home, suffix); + + free(home); + + return cache_home; + } else { + return strdup(cache_home); + } +} diff --git a/src/xdg-basedir/xdg-basedir.h b/src/xdg-basedir/xdg-basedir.h new file mode 100644 index 0000000..17c5659 --- /dev/null +++ b/src/xdg-basedir/xdg-basedir.h @@ -0,0 +1,40 @@ +#ifndef XDG_BASEDIR_H +#define XDG_BASEDIR_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * Get user's home directory. Convenience wrapper for getenv("HOME"). + * Returns a freshly allocated char array that must be free'd after usage. + */ +char* user_home(); + +/* + * Get XDG config home directory using $XDG_CONFIG_HOME environment variable. + * Falls back to default value ~/.config if environment variable is not set. + * Returns a freshly allocated char array that must be free'd after usage. + */ +char* xdg_config_home(); + +/* + * Get XDG data home directory using $XDG_DATA_HOME environment variable. + * Falls back to default value ~/.local/share if environment variable is not set. + * Returns a freshly allocated char array that must be free'd after usage. + */ +char* xdg_data_home(); + +/* + * Get XDG cache home directory using $XDG_CACHE_HOME environment variable. + * Falls back to default value ~/.cache if environment variable is not set. + * Returns a freshly allocated char array that must be free'd after usage. + */ +char* xdg_cache_home(); + + +#ifdef __cplusplus +} +#endif + +#endif /* XDG_BASEDIR_H */ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..96f3931 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,37 @@ +# build and add test only if tests are enabled +include(CTest) +if(BUILD_TESTING) + cmake_minimum_required(VERSION 3.5) + + set(CMAKE_CXX_STANDARD 98) + set(CMAKE_CXX_STANDARD_REQUIRED ON) + + # global definitions + add_definitions( + -DTEST_DATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data/" + -DGIT_COMMIT="AppImageKit unit tests" + ) + + add_library(fixtures INTERFACE) + target_sources(fixtures INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/fixtures.h) + set_property(TARGET fixtures PROPERTY INTERFACE_LINK_LIBRARIES xdg-basedir gtest) + + add_executable(test_libappimage test_libappimage.cpp) + target_link_libraries(test_libappimage fixtures libappimage libsquashfuse) + add_test(test_libappimage test_libappimage) + + add_executable(test_shared test_shared.cpp) + target_link_libraries(test_shared fixtures libappimage_shared) + add_test(test_shared test_shared) + # needed for some const->non-const conversions + target_compile_options(test_shared PRIVATE -fpermissive) + + add_executable(test-xdg-basedir test-xdg-basedir.cpp) + target_link_libraries(test-xdg-basedir fixtures xdg-basedir) + add_test(test-xdg-basedir test-xdg-basedir) + + add_executable(test_desktop_integration test_desktop_integration.cpp file_management_utils.hpp) + target_include_directories(test_desktop_integration PRIVATE "${PROJECT_SOURCE_DIR}/src/libappimage") + target_link_libraries(test_desktop_integration libappimage libappimage_shared libsquashfuse gtest gtest_main) + add_test(test_desktop_integration test_desktop_integration) +endif() diff --git a/tests/data/AppImageExtract_6-x86_64.AppImage b/tests/data/AppImageExtract_6-x86_64.AppImage new file mode 100755 index 0000000..20c2698 Binary files /dev/null and b/tests/data/AppImageExtract_6-x86_64.AppImage differ diff --git a/tests/data/AppImageExtract_6_no_magic_bytes-x86_64.AppImage b/tests/data/AppImageExtract_6_no_magic_bytes-x86_64.AppImage new file mode 100755 index 0000000..f512825 Binary files /dev/null and b/tests/data/AppImageExtract_6_no_magic_bytes-x86_64.AppImage differ diff --git a/tests/data/Cura.desktop b/tests/data/Cura.desktop new file mode 100644 index 0000000..1a9b615 --- /dev/null +++ b/tests/data/Cura.desktop @@ -0,0 +1,15 @@ +[Desktop Entry] +Name=Ultimaker Cura +Name[de]=Ultimaker Cura +GenericName=3D Printing Software +GenericName[de]=3D-Druck-Software +Comment=Cura converts 3D models into paths for a 3D printer. It prepares your print for maximum accuracy, minimum printing time and good reliability with many extra features that make your print come out great. +Comment[de]=Cura wandelt 3D-Modelle in Pfade für einen 3D-Drucker um. Es bereitet Ihren Druck für maximale Genauigkeit, minimale Druckzeit und guter Zuverlässigkeit mit vielen zusätzlichen Funktionen vor, damit Ihr Druck großartig wird. +Exec=cura %F +TryExec=cura +Icon=cura-icon +Terminal=false +Type=Application +MimeType=application/sla;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;image/bmp;image/gif;image/jpeg;image/png;model/x3d+xml; +Categories=Graphics; +Keywords=3D;Printing; diff --git a/tests/data/Echo-no-integrate-x86_64.AppImage b/tests/data/Echo-no-integrate-x86_64.AppImage new file mode 100755 index 0000000..ab9b793 Binary files /dev/null and b/tests/data/Echo-no-integrate-x86_64.AppImage differ diff --git a/tests/data/Echo-test1234-x86_64.AppImage b/tests/data/Echo-test1234-x86_64.AppImage new file mode 100755 index 0000000..4e893a3 Binary files /dev/null and b/tests/data/Echo-test1234-x86_64.AppImage differ diff --git a/tests/data/Echo-x86_64.AppImage b/tests/data/Echo-x86_64.AppImage new file mode 100755 index 0000000..bd07c23 Binary files /dev/null and b/tests/data/Echo-x86_64.AppImage differ diff --git a/tests/data/appimaged-i686.AppImage b/tests/data/appimaged-i686.AppImage new file mode 100755 index 0000000..40cde74 Binary files /dev/null and b/tests/data/appimaged-i686.AppImage differ diff --git a/tests/data/appimagetool-x86_64.AppImage b/tests/data/appimagetool-x86_64.AppImage new file mode 100755 index 0000000..d07dcc7 Binary files /dev/null and b/tests/data/appimagetool-x86_64.AppImage differ diff --git a/tests/data/elffile b/tests/data/elffile new file mode 100755 index 0000000..ee3f388 Binary files /dev/null and b/tests/data/elffile differ diff --git a/tests/data/minimal.iso b/tests/data/minimal.iso new file mode 100644 index 0000000..a5150b5 Binary files /dev/null and b/tests/data/minimal.iso differ diff --git a/tests/data/squashfs-root/.DirIcon b/tests/data/squashfs-root/.DirIcon new file mode 120000 index 0000000..08ba74f --- /dev/null +++ b/tests/data/squashfs-root/.DirIcon @@ -0,0 +1 @@ +utilities-terminal.svg \ No newline at end of file diff --git a/tests/data/squashfs-root/AppRun b/tests/data/squashfs-root/AppRun new file mode 100755 index 0000000..67e971b Binary files /dev/null and b/tests/data/squashfs-root/AppRun differ diff --git a/tests/data/squashfs-root/echo.desktop b/tests/data/squashfs-root/echo.desktop new file mode 100644 index 0000000..0707d31 --- /dev/null +++ b/tests/data/squashfs-root/echo.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Version=1.0 +Type=Application +Name=Echo +Name[de]=Echo DE +Comment=Just echo. +Exec=echo %F +Icon=utilities-terminal +X-AppImage-Version=1234 diff --git a/tests/data/squashfs-root/usr/bin/echo b/tests/data/squashfs-root/usr/bin/echo new file mode 100755 index 0000000..44f4be9 Binary files /dev/null and b/tests/data/squashfs-root/usr/bin/echo differ diff --git a/tests/data/squashfs-root/utilities-terminal.svg b/tests/data/squashfs-root/utilities-terminal.svg new file mode 100644 index 0000000..4a0250d --- /dev/null +++ b/tests/data/squashfs-root/utilities-terminal.svg @@ -0,0 +1,320 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/file_management_utils.hpp b/tests/file_management_utils.hpp new file mode 100644 index 0000000..313492b --- /dev/null +++ b/tests/file_management_utils.hpp @@ -0,0 +1,70 @@ +#include +#include +#include + +void removeDirRecursively(const std::string& path) { + GDir* tempdir = NULL; + GError* error; + tempdir = g_dir_open(path.c_str(), 0, &error); + if (!tempdir) { + g_warning("%s\n", error->message); + g_error_free(error); + return; + } + + const char* entry; + while ((entry = g_dir_read_name(tempdir)) != NULL) { + char* full_path = g_strjoin("/", path.c_str(), entry, NULL); + if (g_file_test(full_path, G_FILE_TEST_IS_DIR)) { + removeDirRecursively(full_path); + } else + g_remove(full_path); + + free(full_path); + } + + g_rmdir(path.c_str()); + g_dir_close(tempdir); +} + +std::string createTempDir(const std::string& base_name) { + std::string result; + GError* error = NULL; + std::string template_name = base_name + "-XXXXXX"; + char* path = g_dir_make_tmp(template_name.c_str(), &error); + if (error) { + g_warning("%s", error->message); + g_error_free(error); + } else { + result = path; + free(path); + } + + return result; +} + +bool copy_file(const char* source, const char* target) { + char *target_dir = g_path_get_dirname(target); + int res = g_mkdir_with_parents(target_dir, S_IRWXU); + g_free(target_dir); + + if (res == -1) + return false; + + int fin = open(source, O_RDONLY); + int fout = open(target, O_WRONLY | O_CREAT, S_IRWXU); + + if (fin == -1 || fout == -1) + return false; + + const size_t BUF_SIZE = 1024; + char buf[BUF_SIZE]; + size_t bs_read; + while ((bs_read = static_cast(read(fin, &buf, BUF_SIZE))) != 0) + write(fout, &buf, bs_read); + + close(fin); + close(fout); + + return true; +} diff --git a/tests/fixtures.h b/tests/fixtures.h new file mode 100644 index 0000000..dc40832 --- /dev/null +++ b/tests/fixtures.h @@ -0,0 +1,191 @@ +#pragma once + +#include +#include +#include +#include +#include + + +// fixture providing a temporary directory, and a temporary home directory within that directory +// overwrites HOME environment variable to ensure desktop files etc. are not installed in the system +class TestBase : public ::testing::Test { +private: + char* oldHome; + char* oldXdgDataHome; + char* oldXdgConfigHome; + +public: + std::string tempDir; + std::string tempHome; + +protected: + std::string elf_file_path; + std::string iso_9660_file_path; + std::string appImage_type_1_file_path; + std::string appImage_type_1_no_magic_file_path; + std::string appImage_type_2_file_path; + std::string appImage_type_2_versioned_path; + std::string appImage_type_2_terminal_file_path; + std::string appImage_type_2_shall_not_integrate_path; + +public: + TestBase() { + char* tmpl = strdup("/tmp/AppImageKit-unit-tests-XXXXXX"); + tempDir = mkdtemp(tmpl); + free(tmpl); + + tempHome = tempDir + "/HOME"; + + mkdir(tempHome.c_str(), 0700); + + oldHome = getenv("HOME"); + oldXdgDataHome = getenv("XDG_DATA_HOME"); + oldXdgConfigHome = getenv("XDG_CONFIG_HOME"); + + std::string newXdgDataHome = tempHome + "/.local/share"; + std::string newXdgConfigHome = tempHome + "/.config"; + + setenv("HOME", tempHome.c_str(), true); + setenv("XDG_DATA_HOME", newXdgDataHome.c_str(), true); + setenv("XDG_CONFIG_HOME", newXdgConfigHome.c_str(), true); + + char* xdgDataHome = xdg_data_home(); + char* xdgConfigHome = xdg_config_home(); + + EXPECT_EQ(getenv("HOME"), tempHome); + EXPECT_EQ(newXdgDataHome, xdgDataHome); + EXPECT_EQ(newXdgConfigHome, xdgConfigHome); + + free(xdgDataHome); + free(xdgConfigHome); + + iso_9660_file_path = std::string(TEST_DATA_DIR) + "/minimal.iso"; + elf_file_path = std::string(TEST_DATA_DIR) + "/elffile"; + appImage_type_1_file_path = std::string(TEST_DATA_DIR) + "/AppImageExtract_6-x86_64.AppImage"; + appImage_type_1_no_magic_file_path = std::string(TEST_DATA_DIR) + "/AppImageExtract_6_no_magic_bytes-x86_64.AppImage"; + appImage_type_2_file_path = std::string(TEST_DATA_DIR) + "/Echo-x86_64.AppImage"; + appImage_type_2_versioned_path = std::string(TEST_DATA_DIR) + "/Echo-test1234-x86_64.AppImage"; + appImage_type_2_shall_not_integrate_path = std::string(TEST_DATA_DIR) + "/Echo-no-integrate-x86_64.AppImage"; + appImage_type_2_terminal_file_path = std::string(TEST_DATA_DIR) + "/appimagetool-x86_64.AppImage"; + + EXPECT_TRUE(isFile(appImage_type_1_file_path)); + EXPECT_TRUE(isFile(appImage_type_2_file_path)); + EXPECT_TRUE(isFile(appImage_type_2_versioned_path)); + EXPECT_TRUE(isFile(appImage_type_2_terminal_file_path)); + EXPECT_TRUE(isFile(appImage_type_2_shall_not_integrate_path)); + }; + + ~TestBase() { + if (isDir(tempDir)) { + rmTree(tempDir); + } + + if (oldHome != NULL) { + setenv("HOME", oldHome, true); + } else { + unsetenv("HOME"); + } + + if (oldXdgDataHome != NULL) { + setenv("XDG_DATA_HOME", oldXdgDataHome, true); + } else { + unsetenv("XDG_DATA_HOME"); + } + + if (oldXdgConfigHome != NULL) { + setenv("XDG_CONFIG_HOME", oldXdgConfigHome, true); + } else { + unsetenv("XDG_CONFIG_HOME"); + } + } + +private: + static const int rmTree(const std::string& path) { + int rv = nftw(path.c_str(), unlinkCb, 64, FTW_DEPTH|FTW_MOUNT|FTW_PHYS); + + if (rv != 0) { + int error = errno; + std::cerr << "nftw() in rmTree(" << path << ") failed: " << strerror(error) << std::endl; + return rv; + } + + return 0; + } + + static int unlinkCb(const char* fpath, const struct stat* sb, int typeflag, struct FTW* ftwbuf) { + int rv; + + switch (typeflag) { + case FTW_D: + case FTW_DNR: + case FTW_DP: + rv = rmdir(fpath); + break; + default: + rv = unlink(fpath); + break; + } + + return rv; + }; + +public: + static const bool isFile(const std::string& path) { + struct stat st; + + if (stat(path.c_str(), &st) != 0) { + perror("Failed to call stat(): "); + return false; + } + + return S_ISREG(st.st_mode); + } + + static const bool isDir(const std::string& path) { + struct stat st; + + if (stat(path.c_str(), &st) != 0) { + perror("Failed to call stat(): "); + return false; + } + + return S_ISDIR(st.st_mode); + } + + static const std::vector splitString(const std::string& s, char delim = ' ') { + std::vector result; + + std::stringstream ss(s); + std::string item; + + while (std::getline(ss, item, delim)) { + result.push_back(item); + } + + return result; + } + + static const bool isEmptyString(const std::string& str) { + // check whether string is empty beforehand, as the string is interpreted as C string and contains a trailing \0 + if (str.empty()) + return true; + + for (int i = 0; i < str.length(); i++) { + char chr = str[i]; + if (chr != ' ' && chr != '\t') + return false; + } + + return true; + } + + static const bool stringStartsWith(const std::string& str, const std::string& prefix) { + for (int i = 0; i < prefix.length(); i++) { + if (str[i] != prefix[i]) + return false; + } + + return true; + } +}; diff --git a/tests/test-xdg-basedir.cpp b/tests/test-xdg-basedir.cpp new file mode 100644 index 0000000..4ca3647 --- /dev/null +++ b/tests/test-xdg-basedir.cpp @@ -0,0 +1,166 @@ +// system headers +#include +#include + +// library headers +#include + +// local headers +#include "xdg-basedir.h" + +bool compareStrings(const char* str1, const char* str2) { + if (str1 == NULL || str2 == NULL) + return false; + + return strcmp(str1, str2) == 0; +} + +TEST(xdg_basedir_test, test_user_home_default_value) { + char* home = user_home(); + EXPECT_PRED2(compareStrings, home, getenv("HOME")); + free(home); +} + +TEST(xdg_basedir_test, test_user_home_custom_value) { + char* oldValue = strdup(getenv("HOME")); + setenv("HOME", "ABCDEFG", true); + + char* currentValue = user_home(); + EXPECT_PRED2(compareStrings, currentValue, getenv("HOME")); + EXPECT_PRED2(compareStrings, currentValue, "ABCDEFG"); + free(currentValue); + + setenv("HOME", oldValue, true); + free(oldValue); +} + +TEST(xdg_basedir_test, test_xdg_data_home_default_value) { + // make sure env var is not set, to force function to use default value + char* oldValue; + + if ((oldValue = getenv("XDG_DATA_HOME")) != NULL) { + unsetenv("XDG_DATA_HOME"); + } + + char* currentValue = xdg_data_home(); + + // too lazy to calculate size + char* expectedValue = static_cast(malloc(PATH_MAX)); + strcpy(expectedValue, getenv("HOME")); + strcat(expectedValue, "/.local/share"); + + EXPECT_PRED2(compareStrings, currentValue, expectedValue); + + free(expectedValue); + free(currentValue); + + if (oldValue != NULL) { + setenv("XDG_DATA_HOME", oldValue, true); + free(oldValue); + } +} + +TEST(xdg_basedir_test, test_xdg_data_home_custom_value) { + char* oldValue = getenv("XDG_DATA_HOME"); + setenv("XDG_DATA_HOME", "HIJKLM", true); + + char* currentValue = xdg_data_home(); + EXPECT_PRED2(compareStrings, currentValue, "HIJKLM"); + free(currentValue); + + if (oldValue != NULL) { + setenv("XDG_DATA_HOME", oldValue, true); + free(oldValue); + } else { + unsetenv("XDG_DATA_HOME"); + } +} + +TEST(xdg_basedir_test, test_xdg_config_home_default_value) { + // make sure env var is not set, to force function to use default value + char* oldValue; + + if ((oldValue = getenv("XDG_CONFIG_HOME")) != NULL) { + unsetenv("XDG_CONFIG_HOME"); + } + + char* currentValue = xdg_config_home(); + + // too lazy to calculate size + char* expectedValue = static_cast(malloc(PATH_MAX)); + strcpy(expectedValue, getenv("HOME")); + strcat(expectedValue, "/.config"); + + EXPECT_PRED2(compareStrings, currentValue, expectedValue); + + free(expectedValue); + free(currentValue); + + if (oldValue != NULL) { + setenv("XDG_CONFIG_HOME", oldValue, true); + free(oldValue); + } +} + +TEST(xdg_basedir_test, test_xdg_config_home_custom_value) { + char* oldValue = getenv("XDG_CONFIG_HOME"); + setenv("XDG_CONFIG_HOME", "NOPQRS", true); + + char* currentValue = xdg_config_home(); + EXPECT_PRED2(compareStrings, currentValue, "NOPQRS"); + free(currentValue); + + if (oldValue != NULL) { + setenv("XDG_CONFIG_HOME", oldValue, true); + free(oldValue); + } else { + unsetenv("XDG_CONFIG_HOME"); + } +} + +TEST(xdg_basedir_test, test_xdg_cache_home_default_value) { + // make sure env var is not set, to force function to use default value + char* oldValue; + + if ((oldValue = getenv("XDG_CACHE_HOME")) != NULL) { + unsetenv("XDG_CACHE_HOME"); + } + + char* currentValue = xdg_cache_home(); + + // too lazy to calculate size + char* expectedValue = static_cast(malloc(PATH_MAX)); + strcpy(expectedValue, getenv("HOME")); + strcat(expectedValue, "/.cache"); + + EXPECT_PRED2(compareStrings, currentValue, expectedValue); + + free(expectedValue); + free(currentValue); + + if (oldValue != NULL) { + setenv("XDG_CACHE_HOME", oldValue, true); + free(oldValue); + } +} + +TEST(xdg_basedir_test, test_xdg_cache_home_custom_value) { + char* oldValue = getenv("XDG_CACHE_HOME"); + setenv("XDG_CACHE_HOME", "TUVWXY", true); + + char* currentValue = xdg_cache_home(); + EXPECT_PRED2(compareStrings, currentValue, "TUVWXY"); + free(currentValue); + + if (oldValue != NULL) { + setenv("XDG_CACHE_HOME", oldValue, true); + free(oldValue); + } else { + unsetenv("XDG_CACHE_HOME"); + } +} + +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/test_desktop_integration.cpp b/tests/test_desktop_integration.cpp new file mode 100644 index 0000000..6d85447 --- /dev/null +++ b/tests/test_desktop_integration.cpp @@ -0,0 +1,183 @@ +// library headers +#include +#include +#include +#include +#include +extern "C" { +#include +} + +#include "desktop_integration.h" +#include "desktop_file_integration_private.h" +#include "file_management_utils.hpp" + +class DesktopIntegrationTests : public ::testing::Test { +protected: + std::string appdir_path; + std::string user_dir_path; + char* appimage_path; + + virtual void SetUp() { + appimage_path = g_strjoin("/", TEST_DATA_DIR, "Echo-x86_64.AppImage", NULL); + + appdir_path = createTempDir("libappimage-di-appdir"); + user_dir_path = createTempDir("libappimage-di-user-dir"); + + ASSERT_FALSE(appdir_path.empty()); + ASSERT_FALSE(user_dir_path.empty()); + } + + virtual void TearDown() { + removeDirRecursively(appdir_path); + removeDirRecursively(user_dir_path); + + g_free(appimage_path); + } + + void fillMinimalAppDir() { + std::map files; + files["squashfs-root/usr/bin/echo"] = "usr/bin/echo"; + files["squashfs-root/utilities-terminal.svg"] = ".DirIcon"; + files["squashfs-root/AppRun"] = "AppRun"; + files["squashfs-root/echo.desktop"] = "echo.desktop"; + + copy_files(files); + } + + void copy_files(std::map& files) const { + for (std::map::iterator itr = files.begin(); itr != files.end(); itr++) { + std::string source = std::string(TEST_DATA_DIR) + "/" + itr->first; + std::string target = appdir_path + "/" + itr->second; + g_info("Coping %s to %s", source.c_str(), target.c_str()); + copy_file(source.c_str(), target.c_str()); + } + } +}; + +TEST_F(DesktopIntegrationTests, create_remove_tempdir) { + char* tempdir = desktop_integration_create_tempdir(); + ASSERT_TRUE(g_file_test(tempdir, G_FILE_TEST_IS_DIR)); + ASSERT_TRUE(g_file_test(tempdir, G_FILE_TEST_EXISTS)); + + desktop_integration_remove_tempdir(tempdir); + + ASSERT_FALSE(g_file_test(tempdir, G_FILE_TEST_IS_DIR)); + ASSERT_FALSE(g_file_test(tempdir, G_FILE_TEST_EXISTS)); + + free(tempdir); +} + +TEST_F(DesktopIntegrationTests, extract_relevant_files) { + // Test body + desktop_integration_extract_relevant_files(appimage_path, appdir_path.c_str()); + + GDir* tempdir = NULL; + tempdir = g_dir_open(appdir_path.c_str(), 0, NULL); + if (!tempdir) + FAIL(); + + const char* entry; + bool desktop_file_found = false; + while ((entry = g_dir_read_name(tempdir)) != NULL) { + if (g_str_has_suffix(entry, ".Desktop") || g_str_has_suffix(entry, ".desktop")) + desktop_file_found = true; + } + + g_dir_close(tempdir); + ASSERT_TRUE(desktop_file_found); +} + + +char* extract_exec_args_from_desktop(GKeyFile* original_desktop_file) { + char* original_exec_value = g_key_file_get_string(original_desktop_file, + G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_EXEC, NULL); + g_key_file_free(original_desktop_file); + + char** original_exec_value_parts = g_strsplit_set(original_exec_value, " ", 2); + char* original_exec_value_args = NULL; + char** ptr = original_exec_value_parts; + if (*ptr != NULL) + ptr++; + if (*ptr != NULL) + original_exec_value_args = g_strdup(*ptr); + + for (ptr = original_exec_value_parts; *ptr != NULL; ptr++) + free(*ptr); + free(original_exec_value_parts); + free(original_exec_value); + + return original_exec_value_args; +} + +TEST_F(DesktopIntegrationTests, modify_desktop_file) { + // Test SetUp + fillMinimalAppDir(); + + // Test body + char* desktop_file_path = find_desktop_file(appdir_path.c_str()); + ASSERT_TRUE(desktop_file_path); + GKeyFile* original_desktop_file = load_desktop_file(desktop_file_path); + + char* original_desktop_file_args = extract_exec_args_from_desktop(original_desktop_file); + + char* appimage_path_md5 = appimage_get_md5(appimage_path); + bool res = desktop_integration_modify_desktop_file(appimage_path, appdir_path.c_str(), appimage_path_md5); + ASSERT_TRUE(res); + + GKeyFile* desktop_file = load_desktop_file(desktop_file_path); + + char* tryExecValue = g_key_file_get_string(desktop_file, + G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_TRY_EXEC, NULL); + ASSERT_STREQ(tryExecValue, appimage_path); + g_free(tryExecValue); + + char* execValue = g_key_file_get_string(desktop_file, + G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_EXEC, NULL); + + ASSERT_TRUE(g_str_has_prefix(execValue, appimage_path)); + ASSERT_TRUE(g_str_has_suffix(execValue, original_desktop_file_args)); + g_free(original_desktop_file_args); + g_free(execValue); + + char* iconValue = g_key_file_get_string(desktop_file, + G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_ICON, NULL); + + char* expected_icon_prefix = g_strjoin("", "appimagekit_", appimage_path_md5, "_", NULL); + ASSERT_TRUE(g_str_has_prefix(iconValue, expected_icon_prefix)); + g_free(expected_icon_prefix); + g_free(appimage_path_md5); + g_free(iconValue); + + // Test Clean Up + g_key_file_free(desktop_file); + free(desktop_file_path); +} + + +TEST_F(DesktopIntegrationTests, move_files_to_user_data_dir) { + // Test SetUp + fillMinimalAppDir(); + + char* md5sum = appimage_get_md5(appimage_path); + + desktop_integration_modify_desktop_file(appimage_path, appdir_path.c_str(), md5sum); + // Test body + ASSERT_TRUE(desktop_integration_move_files_to_user_data_dir(appdir_path.c_str(), user_dir_path.c_str(), md5sum)); + + /** Validate that the desktop file was copied */ + char* path = g_strjoin("", user_dir_path.c_str(), "/applications/appimagekit_", md5sum, "-echo.desktop", + NULL); + ASSERT_TRUE(g_file_test(path, G_FILE_TEST_EXISTS)); + free(path); + + /** Validate that the icon was copied */ + path = g_strjoin("", user_dir_path.c_str(), "/icons/hicolor/32x32/apps/appimagekit_", md5sum, + "_utilities-terminal.png", + NULL); + ASSERT_TRUE(g_file_test(path, G_FILE_TEST_EXISTS)); + free(path); + + // Test Clean Up + free(md5sum); +} diff --git a/tests/test_getsection.cpp b/tests/test_getsection.cpp new file mode 100644 index 0000000..4546e80 --- /dev/null +++ b/tests/test_getsection.cpp @@ -0,0 +1,27 @@ +// system headers +#include + +// library headers +#include + +// local headers +#include +#include "fixtures.h" + +extern "C" { + #include "getsection.h" +} + + +using namespace std; + + +// most simple derivative class for better naming of the tests in this file +class GetSectionCTest : public AppImageKitTest {}; + + + +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/test_libappimage.cpp b/tests/test_libappimage.cpp new file mode 100644 index 0000000..7ce7337 --- /dev/null +++ b/tests/test_libappimage.cpp @@ -0,0 +1,760 @@ +#include "appimage/appimage.h" + +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include "fixtures.h" + +using namespace std; + +// forward declarations of non-publicly available functions which are needed by some of the tests +// TODO: get rid of those +extern "C" { + bool write_edited_desktop_file(GKeyFile*, const char*, gchar*, int, char*, gboolean); +} + +class LibAppImageTest : public TestBase { +protected: + void rm_file(const std::string &path) { + g_remove(path.c_str()); + } + + bool areIntegrationFilesDeployed(const std::string &path) { + gchar *sum = appimage_get_md5(path.c_str()); + + GDir *dir; + GError *error = NULL; + const gchar *filename = NULL; + + char *data_home = xdg_data_home(); + char *apps_path = g_strconcat(data_home, "/applications/", NULL); + free(data_home); + + bool found = false; + dir = g_dir_open(apps_path, 0, &error); + if (dir != NULL) { + while ((filename = g_dir_read_name(dir))) { + gchar* m = g_strrstr(filename, sum); + + if (m != NULL) + found = true; + } + g_dir_close(dir); + } + g_free(apps_path); + g_free(sum); + return found; + } +}; + +TEST_F(LibAppImageTest, appimage_get_type_invalid) { + ASSERT_EQ(appimage_get_type("/tmp", false), -1); +} + +TEST_F(LibAppImageTest, appimage_get_type_on_bare_iso_9660_file) { + ASSERT_EQ(appimage_get_type(iso_9660_file_path.c_str(), false), -1); +} + +TEST_F(LibAppImageTest, appimage_get_type_on_bare_elf_file) { + ASSERT_EQ(appimage_get_type(elf_file_path.c_str(), false), -1); +} + +TEST_F(LibAppImageTest, appimage_get_type_1) { + ASSERT_EQ(appimage_get_type(appImage_type_1_file_path.c_str(), false), 1); +} + +TEST_F(LibAppImageTest, appimage_get_type_on_appimage_type_1_withouth_magic_bytes) { + ASSERT_EQ(appimage_get_type(appImage_type_1_no_magic_file_path.c_str(), false), 1); +} + +TEST_F(LibAppImageTest, appimage_get_type_2) { + ASSERT_EQ(appimage_get_type(appImage_type_2_file_path.c_str(), false), 2); +} + +TEST_F(LibAppImageTest, appimage_register_in_system_with_type1) { + ASSERT_EQ(appimage_register_in_system(appImage_type_1_file_path.c_str(), false), 0); + + ASSERT_TRUE(areIntegrationFilesDeployed(appImage_type_1_file_path)); + + appimage_unregister_in_system(appImage_type_1_file_path.c_str(), false); +} + +TEST_F(LibAppImageTest, appimage_register_in_system_with_type2) { + ASSERT_EQ(appimage_register_in_system(appImage_type_2_file_path.c_str(), false), 0); + + ASSERT_TRUE(areIntegrationFilesDeployed(appImage_type_2_file_path)); + + appimage_unregister_in_system(appImage_type_2_file_path.c_str(), false); +} + +TEST_F(LibAppImageTest, appimage_type1_register_in_system) { + ASSERT_TRUE(appimage_type1_register_in_system(appImage_type_1_file_path.c_str(), false)); + + ASSERT_TRUE(areIntegrationFilesDeployed(appImage_type_1_file_path)); + + appimage_unregister_in_system(appImage_type_1_file_path.c_str(), false); +} + +TEST_F(LibAppImageTest, appimage_type2_register_in_system) { + EXPECT_TRUE(appimage_type2_register_in_system(appImage_type_2_file_path.c_str(), false)); + + EXPECT_TRUE(areIntegrationFilesDeployed(appImage_type_2_file_path)); + appimage_unregister_in_system(appImage_type_2_file_path.c_str(), false); +} + +TEST_F(LibAppImageTest, appimage_unregister_in_system) { + ASSERT_FALSE(areIntegrationFilesDeployed(appImage_type_1_file_path)); + ASSERT_FALSE(areIntegrationFilesDeployed(appImage_type_2_file_path)); +} + +TEST_F(LibAppImageTest, appimage_get_md5) { + std::string pathToTestFile = "/some/fixed/path"; + + std::string expected = "972f4824b8e6ea26a55e9af60a285af7"; + + gchar *sum = appimage_get_md5(pathToTestFile.c_str()); + EXPECT_EQ(sum, expected); + g_free(sum); + + unlink(pathToTestFile.c_str()); +} + +TEST_F(LibAppImageTest, get_md5_invalid_file_path) { + gchar *sum = appimage_get_md5(""); + + ASSERT_TRUE(sum == NULL) << "sum is not NULL"; +} + +TEST_F(LibAppImageTest, create_thumbnail_appimage_type_1) { + appimage_create_thumbnail(appImage_type_1_file_path.c_str(), false); + + gchar *sum = appimage_get_md5(appImage_type_1_file_path.c_str()); + + char *cache_home = xdg_cache_home(); + std::string path = std::string(cache_home) + + "/thumbnails/normal/" + + std::string(sum) + ".png"; + + g_free(cache_home); + g_free(sum); + + ASSERT_TRUE(g_file_test(path.c_str(), G_FILE_TEST_EXISTS)); + + // Clean + rm_file(path); +} + +TEST_F(LibAppImageTest, create_thumbnail_appimage_type_2) { + appimage_create_thumbnail(appImage_type_2_file_path.c_str(), false); + + gchar *sum = appimage_get_md5(appImage_type_2_file_path.c_str()); + + char* cache_home = xdg_cache_home(); + std::string path = std::string(cache_home) + + "/thumbnails/normal/" + + std::string(sum) + ".png"; + + g_free(cache_home); + g_free(sum); + + ASSERT_TRUE(g_file_test(path.c_str(), G_FILE_TEST_EXISTS)); + + // Clean + rm_file(path); +} + +TEST_F(LibAppImageTest, appimage_extract_file_following_symlinks) { + std::string target_path = tempDir + "test_libappimage_tmp_file"; + appimage_extract_file_following_symlinks(appImage_type_2_file_path.c_str(), "echo.desktop", + target_path.c_str()); + + const char expected[] = ("[Desktop Entry]\n" + "Version=1.0\n" + "Type=Application\n" + "Name=Echo\n" + "Comment=Just echo.\n" + "Exec=echo %F\n" + "Icon=utilities-terminal\n"); + + ASSERT_TRUE(g_file_test(target_path.c_str(), G_FILE_TEST_EXISTS)); + + std::ifstream file(target_path.c_str(), std::ios::binary | std::ios::ate); + std::streamsize size = file.tellg(); + file.seekg(0, std::ios::beg); + + std::vector buffer(static_cast(size)); + if (file.read(buffer.data(), size)) + ASSERT_TRUE(strncmp(expected, buffer.data(), strlen(expected)) == 0); + else + FAIL(); + + // Clean + remove(target_path.c_str()); +} + +TEST_F(LibAppImageTest, appimage_read_file_into_buffer_following_symlinks_type_2) { + char* buf = NULL; + unsigned long bufsize = 0; + bool rv = appimage_read_file_into_buffer_following_symlinks(appImage_type_2_file_path.c_str(), "echo.desktop", &buf, &bufsize); + + // using EXPECT makes sure the free call is executed + EXPECT_TRUE(rv); + EXPECT_TRUE(buf != NULL); + EXPECT_TRUE(bufsize != 0); + + static const char expected[] = ("[Desktop Entry]\n" + "Version=1.0\n" + "Type=Application\n" + "Name=Echo\n" + "Comment=Just echo.\n" + "Exec=echo %F\n" + "Icon=utilities-terminal\n"); + + EXPECT_EQ(bufsize, strlen(expected)); + EXPECT_TRUE(buf != NULL && strncmp(expected, buf, bufsize) == 0); + free(buf); +} + +TEST_F(LibAppImageTest, appimage_read_file_into_buffer_following_symlinks_type_1) { + char* buf = NULL; + unsigned long bufsize = 0; + bool rv = appimage_read_file_into_buffer_following_symlinks(appImage_type_1_file_path.c_str(), "AppImageExtract.desktop", &buf, &bufsize); + + // using EXPECT makes sure the free call is executed + EXPECT_TRUE(rv); + EXPECT_TRUE(buf != NULL); + EXPECT_TRUE(bufsize != 0); + + static const char expected[] = ("[Desktop Entry]\n" + "Name=AppImageExtract\n" + "Exec=appimageextract\n" + "Icon=AppImageExtract\n" + "Terminal=true\n" + "Type=Application\n" + "Categories=Development;\n" + "Comment=Extract AppImage contents, part of AppImageKit\n" + "StartupNotify=true\n"); + + EXPECT_EQ(bufsize, strlen(expected)); + EXPECT_TRUE(buf != NULL && strncmp(expected, buf, bufsize) == 0); + free(buf); +} + + +TEST_F(LibAppImageTest, appimage_read_file_into_buffer_following_hardlinks_type_1) { + char* buf = NULL; + unsigned long bufsize = 0; + bool rv = appimage_read_file_into_buffer_following_symlinks(appImage_type_1_file_path.c_str(), "AppImageExtract.png", &buf, &bufsize); + + // using EXPECT makes sure the free call is executed + EXPECT_TRUE(rv); + EXPECT_TRUE(buf != NULL); + EXPECT_TRUE(bufsize != 0); + + free(buf); +} + +TEST_F(LibAppImageTest, appimage_extract_file_following_hardlinks_type_1) { + const char target_file_path[] = "/tmp/appimage_tmp_file"; + appimage_extract_file_following_symlinks(appImage_type_1_file_path.c_str(), + "AppImageExtract.png", target_file_path); + + + // using EXPECT makes sure the free call is executed + EXPECT_TRUE(g_file_test(target_file_path, G_FILE_TEST_EXISTS)); + EXPECT_TRUE(g_file_test(target_file_path, G_FILE_TEST_IS_REGULAR)); + + struct stat stats = {}; + lstat(target_file_path, &stats); + EXPECT_NE(stats.st_size, 0); + + g_remove(target_file_path); +} + +bool test_appimage_is_registered_in_system(const std::string& pathToAppImage, bool integrateAppImage) { + if (integrateAppImage) { + EXPECT_EQ(appimage_register_in_system(pathToAppImage.c_str(), false), 0); + } + + return appimage_is_registered_in_system(pathToAppImage.c_str()); +} + +TEST_F(LibAppImageTest, appimage_is_registered_in_system) { + // make sure tested AppImages are not registered + appimage_unregister_in_system(appImage_type_1_file_path.c_str(), false); + appimage_unregister_in_system(appImage_type_2_file_path.c_str(), false); + + // if the test order is false -> true, cleanup isn't necessary + + // type 1 tests + EXPECT_FALSE(test_appimage_is_registered_in_system(appImage_type_1_file_path, false)); + EXPECT_TRUE(test_appimage_is_registered_in_system(appImage_type_1_file_path, true)); + + // type 2 tests + EXPECT_FALSE(test_appimage_is_registered_in_system(appImage_type_2_file_path, false)); + EXPECT_TRUE(test_appimage_is_registered_in_system(appImage_type_2_file_path, true)); + + // cleanup + appimage_unregister_in_system(appImage_type_1_file_path.c_str(), false); + appimage_unregister_in_system(appImage_type_2_file_path.c_str(), false); +} + +TEST_F(LibAppImageTest, appimage_list_files_false_appimage) { + + char **files = appimage_list_files("/bin/ls"); + + char *expected[] = {NULL}; + + int i = 0; + for (; files[i] != NULL && expected[i] != NULL; i++) + EXPECT_STREQ(files[i], expected[i]); + + appimage_string_list_free(files); + + if (i != 0) + FAIL(); +} + +TEST_F(LibAppImageTest, appimage_list_files_type_1) { + + char **files = appimage_list_files(appImage_type_1_file_path.c_str()); + + const char *expected[] = { + (char *) "AppImageExtract.desktop", + (char *) ".DirIcon", + (char *) "AppImageExtract.png", + (char *) "usr/bin/appimageextract", + (char *) "AppRun", + (char *) "usr/bin/xorriso", + (char *) "usr/lib/libburn.so.4", + (char *) "usr/lib/libisoburn.so.1", + (char *) "usr/lib/libisofs.so.6", + NULL}; + + int i = 0; + for (; files[i] != NULL && expected[i] != NULL; i++) + EXPECT_STREQ(files[i], expected[i]); + + appimage_string_list_free(files); + if (i != 9) + FAIL(); +} + +TEST_F(LibAppImageTest, appimage_list_files_type_2) { + + char **files = appimage_list_files(appImage_type_2_file_path.c_str()); + + char *expected[] = { + (char *) ".DirIcon", + (char *) "AppRun", + (char *) "echo.desktop", + (char *) "usr", + (char *) "usr/bin", + (char *) "usr/bin/echo", + (char *) "usr/share", + (char *) "usr/share/applications", + (char *) "usr/share/applications/echo.desktop", + (char *) "utilities-terminal.svg", + NULL}; + + int i = 0; + for (; files[i] != NULL && expected[i] != NULL; i++) + EXPECT_STREQ(files[i], expected[i]); + + appimage_string_list_free(files); + if (i != 10) + FAIL(); +} + +TEST_F(LibAppImageTest, test_appimage_registered_desktop_file_path_not_registered) { + EXPECT_TRUE(appimage_registered_desktop_file_path(appImage_type_1_file_path.c_str(), NULL, false) == NULL); + EXPECT_TRUE(appimage_registered_desktop_file_path(appImage_type_2_file_path.c_str(), NULL, false) == NULL); +} + +TEST_F(LibAppImageTest, test_appimage_registered_desktop_file_path_type1) { + EXPECT_TRUE(appimage_type1_register_in_system(appImage_type_1_file_path.c_str(), false)); + + char* desktop_file_path = appimage_registered_desktop_file_path(appImage_type_1_file_path.c_str(), NULL, false); + + EXPECT_TRUE(desktop_file_path != NULL); + + free(desktop_file_path); +} + +TEST_F(LibAppImageTest, test_appimage_registered_desktop_file_path_type2) { + EXPECT_TRUE(appimage_type2_register_in_system(appImage_type_2_file_path.c_str(), false)); + + char* desktop_file_path = appimage_registered_desktop_file_path(appImage_type_2_file_path.c_str(), NULL, false); + + EXPECT_TRUE(desktop_file_path != NULL); + + free(desktop_file_path); +} + +TEST_F(LibAppImageTest, test_appimage_registered_desktop_file_path_type1_precalculated_md5) { + EXPECT_TRUE(appimage_type1_register_in_system(appImage_type_1_file_path.c_str(), false)); + + char* md5 = appimage_get_md5(appImage_type_1_file_path.c_str()); + char* desktop_file_path = appimage_registered_desktop_file_path(appImage_type_1_file_path.c_str(), md5, false); + free(md5); + + EXPECT_TRUE(desktop_file_path != NULL); + + free(desktop_file_path); +} + +TEST_F(LibAppImageTest, test_appimage_registered_desktop_file_path_type2_precalculated_md5) { + EXPECT_TRUE(appimage_type2_register_in_system(appImage_type_2_file_path.c_str(), false)); + + char* md5 = appimage_get_md5(appImage_type_2_file_path.c_str()); + char* desktop_file_path = appimage_registered_desktop_file_path(appImage_type_2_file_path.c_str(), md5, false); + free(md5); + + EXPECT_TRUE(desktop_file_path != NULL); + + free(desktop_file_path); +} + +TEST_F(LibAppImageTest, test_appimage_registered_desktop_file_path_type1_wrong_md5) { + EXPECT_TRUE(appimage_type1_register_in_system(appImage_type_1_file_path.c_str(), false)); + + char* md5 = strdup("abcdefg"); + char* desktop_file_path = appimage_registered_desktop_file_path(appImage_type_1_file_path.c_str(), md5, false); + free(md5); + + EXPECT_TRUE(desktop_file_path == NULL); + + free(desktop_file_path); +} + +TEST_F(LibAppImageTest, test_appimage_registered_desktop_file_path_type2_wrong_md5) { + EXPECT_TRUE(appimage_type2_register_in_system(appImage_type_2_file_path.c_str(), false)); + + char* md5 = strdup("abcdefg"); + char* desktop_file_path = appimage_registered_desktop_file_path(appImage_type_2_file_path.c_str(), md5, false); + free(md5); + + EXPECT_TRUE(desktop_file_path == NULL); + + free(desktop_file_path); +} + +TEST_F(LibAppImageTest, test_appimage_type2_appimage_version) { + EXPECT_TRUE(appimage_type2_register_in_system(appImage_type_2_versioned_path.c_str(), false)); + + char* desktopFilePath = appimage_registered_desktop_file_path(appImage_type_2_versioned_path.c_str(), NULL, false); + + GKeyFile *desktopFile = g_key_file_new(); + + GError* error = NULL; + + gboolean loaded = g_key_file_load_from_file(desktopFile, desktopFilePath, G_KEY_FILE_KEEP_TRANSLATIONS, &error); + + if (!loaded) { + g_key_file_free(desktopFile); + ADD_FAILURE() << "Failed to read desktop file: " << error->message; + g_error_free(error); + return; + } + + const std::string versionKey = "X-AppImage-Version"; + const std::string oldNameKey = "X-AppImage-Old-Name"; + + std::string expectedVersion = "test1234"; + gchar* actualVersion = g_key_file_get_string(desktopFile, G_KEY_FILE_DESKTOP_GROUP, versionKey.c_str(), &error); + + if (actualVersion == NULL) { + g_key_file_free(desktopFile); + ADD_FAILURE() << "Failed to get " << versionKey << " key: " << error->message; + g_error_free(error); + return; + } + + EXPECT_EQ(expectedVersion, std::string(actualVersion)); + + gchar* oldName = g_key_file_get_string(desktopFile, G_KEY_FILE_DESKTOP_GROUP, oldNameKey.c_str(), &error); + + if (oldName == NULL) { + g_key_file_free(desktopFile); + ADD_FAILURE() << "Failed to get " << oldNameKey << " key: " << error->message; + g_error_free(error); + return; + } + + gchar* newName = g_key_file_get_string(desktopFile, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_NAME, &error); + + if (newName == NULL) { + g_key_file_free(desktopFile); + ADD_FAILURE() << "Failed to get " << G_KEY_FILE_DESKTOP_KEY_NAME << " key: " << error->message; + g_error_free(error); + return; + } + + std::string expectedName = std::string(oldName) + " (" + expectedVersion + ")"; + + EXPECT_EQ(expectedName, std::string(newName)); + + // cleanup + g_key_file_free(desktopFile); + if (error != NULL) + g_error_free(error); +} + +TEST_F(LibAppImageTest, test_try_exec_key_exists_type_1) { + const std::string& pathToAppImage = appImage_type_1_file_path; + + ASSERT_EQ(appimage_register_in_system(pathToAppImage.c_str(), false), 0); + + GKeyFile* kf = g_key_file_new(); + + const char* desktopFilePath = appimage_registered_desktop_file_path(pathToAppImage.c_str(), NULL, false); + + ASSERT_TRUE(g_key_file_load_from_file(kf, desktopFilePath, G_KEY_FILE_NONE, NULL)); + + const char* expectedTryExecValue = g_key_file_get_string(kf, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_TRY_EXEC, NULL); + + EXPECT_EQ(expectedTryExecValue, pathToAppImage); +} + +TEST_F(LibAppImageTest, test_try_exec_key_exists_type_2) { + const std::string& pathToAppImage = appImage_type_2_file_path; + + ASSERT_EQ(appimage_register_in_system(pathToAppImage.c_str(), false), 0); + + GKeyFile* kf = g_key_file_new(); + + const char* desktopFilePath = appimage_registered_desktop_file_path(pathToAppImage.c_str(), NULL, false); + + ASSERT_TRUE(g_key_file_load_from_file(kf, desktopFilePath, G_KEY_FILE_NONE, NULL)); + + const char* expectedTryExecValue = g_key_file_get_string(kf, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_TRY_EXEC, NULL); + + EXPECT_EQ(expectedTryExecValue, pathToAppImage); +} + +TEST_F(LibAppImageTest, test_appimage_type1_is_terminal_app) { + // TODO: add type 1 AppImage with Terminal=false + EXPECT_EQ(appimage_type1_is_terminal_app(appImage_type_1_file_path.c_str()), 1); + EXPECT_EQ(appimage_type1_is_terminal_app(appImage_type_2_file_path.c_str()), -1); + EXPECT_EQ(appimage_type1_is_terminal_app("/invalid/path"), -1); +} + +TEST_F(LibAppImageTest, test_appimage_type2_is_terminal_app) { + EXPECT_EQ(appimage_type2_is_terminal_app(appImage_type_1_file_path.c_str()), -1); + EXPECT_EQ(appimage_type2_is_terminal_app(appImage_type_2_terminal_file_path.c_str()), 1); + EXPECT_EQ(appimage_type2_is_terminal_app(appImage_type_2_file_path.c_str()), 0); + EXPECT_EQ(appimage_type2_is_terminal_app("/invalid/path"), -1); +} + +TEST_F(LibAppImageTest, test_appimage_is_terminal_app) { + EXPECT_EQ(appimage_is_terminal_app(appImage_type_1_file_path.c_str()), 1); + EXPECT_EQ(appimage_is_terminal_app(appImage_type_2_file_path.c_str()), 0); + // TODO: add type 1 AppImage with Terminal=true + //EXPECT_EQ(appimage_is_terminal_app(appImage_type_1_terminal_file_path.c_str()), 1); + EXPECT_EQ(appimage_is_terminal_app(appImage_type_2_terminal_file_path.c_str()), 1); + EXPECT_EQ(appimage_is_terminal_app("/invalid/path"), -1); +} + +TEST_F(LibAppImageTest, test_appimage_type1_shall_not_integrate) { + // TODO: add type 1 AppImage with X-AppImage-Integrate=false + //EXPECT_EQ(appimage_is_terminal_app(appImage_type_1_shall_not_integrate_path.c_str()), 1); + EXPECT_EQ(appimage_type1_shall_not_be_integrated(appImage_type_1_file_path.c_str()), 0); + EXPECT_EQ(appimage_type1_shall_not_be_integrated(appImage_type_2_file_path.c_str()), -1); + EXPECT_EQ(appimage_type1_shall_not_be_integrated("/invalid/path"), -1); +} + +TEST_F(LibAppImageTest, test_appimage_type2_shall_not_integrate) { + EXPECT_EQ(appimage_type2_shall_not_be_integrated(appImage_type_1_file_path.c_str()), -1); + EXPECT_EQ(appimage_type2_shall_not_be_integrated(appImage_type_2_shall_not_integrate_path.c_str()), 1); + EXPECT_EQ(appimage_type2_shall_not_be_integrated(appImage_type_2_file_path.c_str()), 0); + EXPECT_EQ(appimage_type2_shall_not_be_integrated("/invalid/path"), -1); +} + +TEST_F(LibAppImageTest, test_appimage_shall_not_integrate) { + EXPECT_EQ(appimage_shall_not_be_integrated(appImage_type_1_file_path.c_str()), 0); + EXPECT_EQ(appimage_shall_not_be_integrated(appImage_type_2_file_path.c_str()), 0); + // TODO: add type 1 AppImage with X-AppImage-Integrate=false + //EXPECT_EQ(appimage_shall_not_be_integrated(appImage_type_1_shall_not_integrate_path.c_str()), 1); + EXPECT_EQ(appimage_shall_not_be_integrated(appImage_type_2_shall_not_integrate_path.c_str()), 1); + EXPECT_EQ(appimage_is_terminal_app("/invalid/path"), -1); +} + +// compares whether the size first bytes of two given byte buffers are equal +bool test_compare_bytes(const char* buf1, const char* buf2, int size) { + for (int i = 0; i < size; i++) { + if (buf1[i] != buf2[i]) { + return false; + } + } + + return true; +} + +TEST_F(LibAppImageTest, appimage_type2_digest_md5) { + char digest[16]; + char expectedDigest[] = {-75, -71, 106, -93, 122, 114, 7, 127, -40, 10, -115, -82, -73, 115, -19, 1}; + + EXPECT_TRUE(appimage_type2_digest_md5(appImage_type_2_file_path.c_str(), digest)); + EXPECT_PRED3(test_compare_bytes, digest, expectedDigest, 16); +} + +TEST_F(LibAppImageTest, test_write_desktop_file_exec) { + // install Cura desktop file into temporary HOME with some hardcoded paths + stringstream pathToOriginalDesktopFile; + pathToOriginalDesktopFile << TEST_DATA_DIR << "/" << "Cura.desktop"; + ifstream ifs(pathToOriginalDesktopFile.str().c_str()); + + ASSERT_TRUE(ifs) << "Failed to open file: " << pathToOriginalDesktopFile.str(); + + ifs.seekg(0, ios::end); + unsigned long bufferSize = static_cast(ifs.tellg() + 1); + ifs.seekg(0, ios::beg); + + // should be large enough + vector buffer(bufferSize, '\0'); + + // read in desktop file + ifs.read(buffer.data(), buffer.size()); + + GError* error = NULL; + + GKeyFile *keyFile = g_key_file_new(); + gboolean success = g_key_file_load_from_data(keyFile, buffer.data(), buffer.size(), (GKeyFileFlags) (G_KEY_FILE_KEEP_COMMENTS | G_KEY_FILE_KEEP_TRANSLATIONS), &error); + + ASSERT_TRUE(error == NULL) << "Error while creating key file from data: " << error->message; + + gchar desktop_filename[] = "desktop_filename"; + gchar md5testvalue[] = "md5testvalue"; + + if (success) { + write_edited_desktop_file(keyFile, "testpath", desktop_filename, 1, md5testvalue, false); + } + + g_key_file_free(keyFile); + + stringstream pathToInstalledDesktopFile; + pathToInstalledDesktopFile << tempHome << g_strdup_printf("/.local/share/applications/appimagekit_%s-%s", md5testvalue, desktop_filename); + + // now, read original and installed desktop file, and compare both + ifstream originalStrm(pathToOriginalDesktopFile.str().c_str()); + ifstream installedStrm(pathToInstalledDesktopFile.str().c_str()); + + ASSERT_TRUE(originalStrm) << "Failed to open desktop file " << pathToOriginalDesktopFile.str(); + ASSERT_TRUE(installedStrm) << "Failed to open desktop file " << pathToInstalledDesktopFile.str(); + + originalStrm.seekg(0, ios::end); + unsigned long originalStrmSize = static_cast(originalStrm.tellg() + 1); + originalStrm.seekg(0, ios::beg); + + installedStrm.seekg(0, ios::end); + unsigned long installedStrmSize = static_cast(installedStrm.tellg() + 1); + installedStrm.seekg(0, ios::beg); + + // split both files by lines, then convert to key-value list, and check whether all lines from original file + // are also available in the installed file + // some values modified by write_edited_desktop_file need some extra checks, which can be performed then. + vector originalData(originalStrmSize, '\0'); + vector installedData(installedStrmSize, '\0'); + + originalStrm.read(originalData.data(), originalData.size()); + installedStrm.read(installedData.data(), installedData.size()); + + vector originalLines = splitString(originalData.data(), '\n'); + vector installedLines = splitString(installedData.data(), '\n'); + // first of all, remove all empty lines + // ancient glib versions like the ones CentOS 6 provides tend to introduce a blank line before the + // [Desktop Entry] header, hence the blank lines need to be stripped out before the next step + originalLines.erase(std::remove_if(originalLines.begin(), originalLines.end(), isEmptyString), originalLines.end()); + installedLines.erase(std::remove_if(installedLines.begin(), installedLines.end(), isEmptyString), installedLines.end()); + // first line should be "[Desktop Entry]" header + ASSERT_EQ(originalLines.front(), "[Desktop Entry]"); + ASSERT_EQ(installedLines.front(), "[Desktop Entry]"); + // drop "[Desktop Entry]" header + originalLines.erase(originalLines.begin()); + installedLines.erase(installedLines.begin()); + + // now, create key-value maps + map entries; + + // sort original entries into map + for (vector::const_iterator line = originalLines.begin(); line != originalLines.end(); line++) { + vector lineSplit = splitString(*line, '='); + ASSERT_EQ(lineSplit.size(), 2) << "line: " << *line; + entries.insert(std::make_pair(lineSplit[0], lineSplit[1])); + } + + // now, remove all entries found in installed desktop entry from entries + for (vector::iterator line = installedLines.begin(); line != installedLines.end();) { + vector lineSplit = splitString(*line, '='); + ASSERT_EQ(lineSplit.size(), 2) << "Condition failed for line: " << *line; + + const string& key = lineSplit[0]; + const string& value = lineSplit[1]; + + if (stringStartsWith(key, "X-AppImage-")) { + // skip this entry + line++; + continue; + } + + map::const_iterator entry = entries.find(key); + + if (entry == entries.end()) + FAIL() << "No such entry in desktop file: " << key; + + if (key == "Exec" || key == "TryExec") { + vector execSplit = splitString(value); + ASSERT_GT(execSplit.size(), 0) << "key: " << key; + ASSERT_EQ(execSplit[0], "testpath") << "key: " << key; + + vector originalExecSplit = splitString((*entry).second); + ASSERT_EQ(execSplit.size(), originalExecSplit.size()) + << key << ": " << value << " and " << (*entry).second << " contain different number of parameters"; + + // the rest of the split parts should be equal + for (int i = 1; i < execSplit.size(); i++) { + ASSERT_EQ(execSplit[i], originalExecSplit[i]); + } + } else if (key == "Icon") { + ASSERT_EQ(value, g_strdup_printf("appimagekit_%s_cura-icon", md5testvalue)); + } else { + ASSERT_EQ(value, (*entry).second); + } + + installedLines.erase(line); + } + + // finally, handle X-AppImage- entries + for (vector::iterator line = installedLines.begin(); line != installedLines.end();) { + if (stringStartsWith(*line, "X-AppImage-Comment")) { + ASSERT_EQ(*line, "X-AppImage-Comment=Generated by appimaged AppImageKit unit tests"); + } else if (stringStartsWith(*line, "X-AppImage-Identifier")) { + ASSERT_EQ(*line, g_strdup_printf("X-AppImage-Identifier=%s", md5testvalue)); + } else if (stringStartsWith(*line, "X-AppImage-Old-")) { + // skip "old" entries, created by localization support + } else { + line++; + continue; + } + + installedLines.erase(line); + } + + ASSERT_EQ(installedLines.size(), 0); +} + +int main(int argc, char **argv) { + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/test_shared.cpp b/tests/test_shared.cpp new file mode 100644 index 0000000..66fcc56 --- /dev/null +++ b/tests/test_shared.cpp @@ -0,0 +1,94 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "fixtures.h" + +extern "C" { + #include +} + + +using namespace std; + + +// most simple derivative class for better naming of the tests in this file +class LibAppImageSharedTest : public TestBase {}; + + +static bool test_strcmp(char* a, char* b) { + return strcmp(a, b) == 0; +} + +TEST_F(LibAppImageSharedTest, test_appimage_hexlify) { + { + char bytesIn[] = "\x00\x01\x02\x03\x04\x05\x06\x07"; + char expectedHex[] = "0001020304050607"; + + char* hexlified = appimage_hexlify(bytesIn, 8); + EXPECT_PRED2(test_strcmp, hexlified, expectedHex); + + // cleanup + free(hexlified); + } + { + char bytesIn[] = "\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"; + char expectedHex[] = "f8f9fafbfcfdfeff"; + + char* hexlified = appimage_hexlify(bytesIn, 8); + EXPECT_PRED2(test_strcmp, hexlified, expectedHex); + + // cleanup + free(hexlified); + } +} + + +bool isPowerOfTwo(int number) { + return (number & (number - 1)) == 0; +} + + +TEST_F(LibAppImageSharedTest, test_appimage_get_elf_section_offset_and_length) { + std::string appImagePath = std::string(TEST_DATA_DIR) + "/appimaged-i686.AppImage"; + + unsigned long offset, length; + + ASSERT_TRUE(appimage_get_elf_section_offset_and_length(appImagePath.c_str(), ".upd_info", &offset, &length)); + + EXPECT_GT(offset, 0); + EXPECT_GT(length, 0); + + EXPECT_PRED1(isPowerOfTwo, length); +} + + +TEST_F(LibAppImageSharedTest, test_print_binary) { + std::string appImagePath = std::string(TEST_DATA_DIR) + "/appimaged-i686.AppImage"; + + unsigned long offset, length; + + ASSERT_TRUE(appimage_get_elf_section_offset_and_length(appImagePath.c_str(), ".upd_info", &offset, &length)); + + EXPECT_EQ(appimage_print_binary(appImagePath.c_str(), offset, length), 0); +} + + +TEST_F(LibAppImageSharedTest, test_print_hex) { + std::string appImagePath = std::string(TEST_DATA_DIR) + "/appimaged-i686.AppImage"; + + unsigned long offset, length; + + ASSERT_TRUE(appimage_get_elf_section_offset_and_length(appImagePath.c_str(), ".sha256_sig", &offset, &length)); + + EXPECT_EQ(appimage_print_hex(appImagePath.c_str(), offset, length), 0); +} + +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/travis/build-and-test.sh b/travis/build-and-test.sh new file mode 100755 index 0000000..5e8571d --- /dev/null +++ b/travis/build-and-test.sh @@ -0,0 +1,36 @@ +#! /bin/bash + +set -e +set -x + +# use RAM disk if possible +if [ "$CI" == "" ] && [ -d /dev/shm ]; then + TEMP_BASE=/dev/shm +else + TEMP_BASE=/tmp +fi + +BUILD_DIR=$(mktemp -d -p "$TEMP_BASE" AppImageUpdate-build-XXXXXX) + +cleanup () { + if [ -d "$BUILD_DIR" ]; then + rm -rf "$BUILD_DIR" + fi +} + +trap cleanup EXIT + +# store repo root as variable +REPO_ROOT=$(readlink -f $(dirname $(dirname $0))) +OLD_CWD=$(readlink -f .) + +pushd "$BUILD_DIR" + +# configure build +cmake "$REPO_ROOT" + +# build binaries +make -j$(nproc) + +# run all unit tests +ctest -V diff --git a/travis/travis-build.sh b/travis/travis-build.sh new file mode 100755 index 0000000..d4f4bba --- /dev/null +++ b/travis/travis-build.sh @@ -0,0 +1,19 @@ +#! /bin/bash + +set -e +set -x + +# build libappimage, and run unit tests +if [ "$DOCKER_IMAGE" != "" ]; then + docker run --rm \ + --cap-add SYS_ADMIN \ + --device /dev/fuse:mrw \ + -e ARCH -e TRAVIS -e TRAVIS_BUILD_NUMBER \ + -e CI=1 \ + -i \ + -v "${PWD}":/libappimage \ + "$DOCKER_IMAGE" \ + /bin/bash -xc "cd /libappimage && travis/build-and-test.sh" +else + exec $(readlink -f $(dirname "$0"))/build-and-test.sh +fi